diff --git a/.dockerignore b/.dockerignore index a883e89c..ecd919cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,4 +28,7 @@ LICENSE CONTRIBUTING.md dist .git -config/ \ No newline at end of file +migrations/ +config/ +build.ts +tsconfig.json \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/feature-requests.yml b/.github/DISCUSSION_TEMPLATE/feature-requests.yml new file mode 100644 index 00000000..03b580ca --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/feature-requests.yml @@ -0,0 +1,47 @@ +body: + - type: textarea + attributes: + label: Summary + description: A clear and concise summary of the requested feature. + validations: + required: true + + - type: textarea + attributes: + label: Motivation + description: | + Why is this feature important? + Explain the problem this feature would solve or what use case it would enable. + validations: + required: true + + - type: textarea + attributes: + label: Proposed Solution + description: | + How would you like to see this feature implemented? + Provide as much detail as possible about the desired behavior, configuration, or changes. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives Considered + description: Describe any alternative solutions or workarounds you've thought about. + validations: + required: false + + - type: textarea + attributes: + label: Additional Context + description: Add any other context, mockups, or screenshots about the feature request here. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting, please: + - Check if there is an existing issue for this feature. + - Clearly explain the benefit and use case. + - Be as specific as possible to help contributors evaluate and implement. diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 00000000..41dbe7bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,51 @@ +name: Bug Report +description: Create a bug report +labels: [] +body: + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: Environment + description: Please fill out the relevant details below for your environment. + value: | + - OS Type & Version: (e.g., Ubuntu 22.04) + - Pangolin Version: + - Gerbil Version: + - Traefik Version: + - Newt Version: + - Olm Version: (if applicable) + validations: + required: true + + - type: textarea + attributes: + label: To Reproduce + description: | + Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. + + If using code blocks, make sure syntax highlighting is correct and double-check that the rendered preview is not broken. + validations: + required: true + + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: markdown + attributes: + value: | + Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + + - type: markdown + attributes: + value: | + Contributors should be able to follow the steps provided in order to reproduce the bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..a3739c4d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Need help or have questions? + url: https://github.com/orgs/fosrl/discussions + about: Ask questions, get help, and discuss with other community members + - name: Request a Feature + url: https://github.com/orgs/fosrl/discussions/new?category=feature-requests + about: Feature requests should be opened as discussions so others can upvote and comment diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 111b0222..0d2008f1 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -3,12 +3,12 @@ name: CI/CD Pipeline on: push: tags: - - "*" + - "[0-9]+.[0-9]+.[0-9]+" jobs: release: name: Build and Release - runs-on: ubuntu-latest + runs-on: amd64-runner steps: - name: Checkout code diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f2129300..1a01f1c4 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -26,9 +26,10 @@ jobs: node-version: '22' - name: Install dependencies - run: | - npm ci + run: npm ci + + - name: Create build file + run: npm run set:oss - name: Run ESLint - run: | - npx eslint . --ext .js,.jsx,.ts,.tsx \ No newline at end of file + run: npx eslint . --ext .js,.jsx,.ts,.tsx \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d22c300..3d121f68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,10 @@ jobs: run: npm ci - name: Create database index.ts - run: echo 'export * from "./sqlite";' > server/db/index.ts + run: npm run set:sqlite + + - name: Create build file + run: npm run set:oss - name: Generate database migrations run: npm run db:sqlite:generate @@ -32,6 +35,9 @@ jobs: - name: Apply database migrations run: npm run db:sqlite:push + - name: Test with tsc + run: npx tsc --noEmit + - name: Start app in background run: nohup npm run dev & diff --git a/.gitignore b/.gitignore index 95b1b9be..a51ed1fa 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,10 @@ next-env.d.ts migrations tsconfig.tsbuildinfo config/config.yml +config/config.saas.yml +config/config.oss.yml +config/config.enterprise.yml +config/privateConfig.yml config/postgres config/postgres* config/openapi.yaml @@ -43,4 +47,6 @@ server/db/index.ts server/build.ts postgres/ dynamic/ -certificates/ +*.mmdb +scratch/ +/tmp diff --git a/Dockerfile b/Dockerfile index 996ef057..0c869763 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,20 @@ COPY . . RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts -RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts +RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi -RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi +RUN mkdir -p dist +RUN npm run next:build +RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs +RUN if [ "$DATABASE" = "pg" ]; then \ + node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \ + else \ + node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \ + fi + +# test to make sure the build output is there and error if not +RUN test -f dist/server.mjs -RUN npm run build:$DATABASE RUN npm run build:cli FROM node:22-alpine AS runner @@ -25,10 +34,11 @@ FROM node:22-alpine AS runner WORKDIR /app # Curl used for the health checks -RUN apk add --no-cache curl +RUN apk add --no-cache curl tzdata # COPY package.json package-lock.json ./ COPY package*.json ./ + RUN npm ci --omit=dev && npm cache clean --force COPY --from=builder /app/.next/standalone ./ @@ -40,7 +50,6 @@ COPY ./cli/wrapper.sh /usr/local/bin/pangctl RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs COPY server/db/names.json ./dist/names.json - COPY public ./public CMD ["npm", "run", "start"] diff --git a/Makefile b/Makefile index de67a5f2..c90bd180 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,48 @@ .PHONY: build build-pg build-release build-arm build-x86 test clean +major_tag := $(shell echo $(tag) | cut -d. -f1) +minor_tag := $(shell echo $(tag) | cut -d. -f1,2) build-release: @if [ -z "$(tag)" ]; then \ echo "Error: tag is required. Usage: make build-release tag="; \ exit 1; \ fi - docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push . - docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push . - docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push . - docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push . + docker buildx build \ + --build-arg BUILD=oss \ + --build-arg DATABASE=sqlite \ + --platform linux/arm64,linux/amd64 \ + --tag fosrl/pangolin:latest \ + --tag fosrl/pangolin:$(major_tag) \ + --tag fosrl/pangolin:$(minor_tag) \ + --tag fosrl/pangolin:$(tag) \ + --push . + docker buildx build \ + --build-arg BUILD=oss \ + --build-arg DATABASE=pg \ + --platform linux/arm64,linux/amd64 \ + --tag fosrl/pangolin:postgresql-latest \ + --tag fosrl/pangolin:postgresql-$(major_tag) \ + --tag fosrl/pangolin:postgresql-$(minor_tag) \ + --tag fosrl/pangolin:postgresql-$(tag) \ + --push . + docker buildx build \ + --build-arg BUILD=enterprise \ + --build-arg DATABASE=sqlite \ + --platform linux/arm64,linux/amd64 \ + --tag fosrl/pangolin:ee-latest \ + --tag fosrl/pangolin:ee-$(major_tag) \ + --tag fosrl/pangolin:ee-$(minor_tag) \ + --tag fosrl/pangolin:ee-$(tag) \ + --push . + docker buildx build \ + --build-arg BUILD=enterprise \ + --build-arg DATABASE=pg \ + --platform linux/arm64,linux/amd64 \ + --tag fosrl/pangolin:ee-postgresql-latest \ + --tag fosrl/pangolin:ee-postgresql-$(major_tag) \ + --tag fosrl/pangolin:ee-postgresql-$(minor_tag) \ + --tag fosrl/pangolin:ee-postgresql-$(tag) \ + --push . build-arm: docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . diff --git a/README.md b/README.md index 982ae140..f7f4ec30 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,36 @@

- - - Pangolin Logo + + + + Pangolin Logo +

-

Secure gateway to your private networks

-
- -_Pangolin tunnels your services to the internet so you can access anything from anywhere._ - -
-
Website | - - Quick Install Guide + + Documentation | Contact Us - | - - Slack - - | - - Discord -
+
+
+ +[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) [![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://digpangolin.com/slack) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) -[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) [![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
@@ -51,108 +41,51 @@ _Pangolin tunnels your services to the internet so you can access anything from

-Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. - -Preview - -![gif](public/clip.gif) +Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN. This is a fork of Pangolin with all proprietary code removed. Proprietary and paywalled features will be reimplemented under the AGPL license. -## Key Features +## Installation -### Reverse Proxy Through WireGuard Tunnel - -- Expose private resources on your network **without opening ports** (firewall punching). -- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt). -- Built-in support for any WireGuard client. -- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/). -- Support for HTTP/HTTPS and **raw TCP/UDP services**. -- Load balancing. -- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock). - - **Automatically install and configure Crowdsec via Pangolin's installer script.** -- Attach as many sites to the central server as you wish. - -### Identity & Access Management - -- Centralized authentication system using platform SSO. **Users will only have to manage one login.** -- **Define access control rules for IPs, IP ranges, and URL paths per resource.** -- TOTP with backup codes for two-factor authentication. -- Create organizations, each with multiple sites, users, and roles. -- **Role-based access control** to manage resource access permissions. -- Additional authentication options include: - - Email whitelisting with **one-time passcodes.** - - **Temporary, self-destructing share links.** - - Resource specific pin codes. - - Resource specific passwords. - - Passkeys -- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others. - - Auto-provision users and roles from your IdP. - -Auth and diagram - -## Use Cases - -### Manage Access to Internal Apps - -- Grant users access to your apps from anywhere using just a web browser. No client software required. - -### Developers and DevOps - -- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access. - -### Secure API Gateway - -- One application load balancer across multiple clouds and on-premises. - -### IoT and Edge Devices - -- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring. - -Sites +Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick-install) for how to install and set up Pangolin. ## Deployment Options -### Fully Self Hosted +| | Description | +|-----------------|--------------| +| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. | +| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. | +| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. | -Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started. +## Key Features -> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal! +Pangolin packages everything you need for seamless application access and exposure into one cohesive platform. -### Pangolin Cloud +| | | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| +| **Manage applications in one place**

Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | | +| **Reverse proxy across networks anywhere**

Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | | +| **Enforce identity and context aware rules**

Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | | +| **Quickly connect Pangolin sites**

Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | | -Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup). +## Get Started -- Everything you get with self hosted Pangolin, but fully managed for you. +### Check out the docs -### Managed & High Availability +We encourage everyone to read the full documentation first, which is +available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of +the docs to illustrate some basic ideas. -Managed control plane, your infrastructure +### Sign up and try now -- We manage database and control plane. -- You self-host lightweight exit-node. -- Traffic flows through your infra. -- We coordinate failover between your nodes or to Cloud when things go bad. - -Try it out using [Pangolin Cloud](https://pangolin.fossorial.io) - -### Full Enterprise On-Premises - -[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team. - -## Project Development / Roadmap - -We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests). +For Pangolin's managed service, you will first need to create an account at +[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started. ## Licensing -Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). +Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io). ## Contributions -Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue. - Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. - -Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository. diff --git a/bruno/API Keys/Create API Key.bru b/bruno/API Keys/Create API Key.bru new file mode 100644 index 00000000..009b4b04 --- /dev/null +++ b/bruno/API Keys/Create API Key.bru @@ -0,0 +1,17 @@ +meta { + name: Create API Key + type: http + seq: 1 +} + +put { + url: http://localhost:3000/api/v1/api-key + body: json + auth: inherit +} + +body:json { + { + "isRoot": true + } +} diff --git a/bruno/API Keys/Delete API Key.bru b/bruno/API Keys/Delete API Key.bru new file mode 100644 index 00000000..9285f788 --- /dev/null +++ b/bruno/API Keys/Delete API Key.bru @@ -0,0 +1,11 @@ +meta { + name: Delete API Key + type: http + seq: 2 +} + +delete { + url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj + body: none + auth: inherit +} diff --git a/bruno/API Keys/List API Key Actions.bru b/bruno/API Keys/List API Key Actions.bru new file mode 100644 index 00000000..ae5b721e --- /dev/null +++ b/bruno/API Keys/List API Key Actions.bru @@ -0,0 +1,11 @@ +meta { + name: List API Key Actions + type: http + seq: 6 +} + +get { + url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions + body: none + auth: inherit +} diff --git a/bruno/API Keys/List Org API Keys.bru b/bruno/API Keys/List Org API Keys.bru new file mode 100644 index 00000000..468e964b --- /dev/null +++ b/bruno/API Keys/List Org API Keys.bru @@ -0,0 +1,11 @@ +meta { + name: List Org API Keys + type: http + seq: 4 +} + +get { + url: http://localhost:3000/api/v1/org/home-lab/api-keys + body: none + auth: inherit +} diff --git a/bruno/API Keys/List Root API Keys.bru b/bruno/API Keys/List Root API Keys.bru new file mode 100644 index 00000000..8ef31b68 --- /dev/null +++ b/bruno/API Keys/List Root API Keys.bru @@ -0,0 +1,11 @@ +meta { + name: List Root API Keys + type: http + seq: 3 +} + +get { + url: http://localhost:3000/api/v1/root/api-keys + body: none + auth: inherit +} diff --git a/bruno/API Keys/Set API Key Actions.bru b/bruno/API Keys/Set API Key Actions.bru new file mode 100644 index 00000000..54a35c43 --- /dev/null +++ b/bruno/API Keys/Set API Key Actions.bru @@ -0,0 +1,17 @@ +meta { + name: Set API Key Actions + type: http + seq: 5 +} + +post { + url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions + body: json + auth: inherit +} + +body:json { + { + "actionIds": ["listSites"] + } +} diff --git a/bruno/API Keys/Set API Key Orgs.bru b/bruno/API Keys/Set API Key Orgs.bru new file mode 100644 index 00000000..3f0676c5 --- /dev/null +++ b/bruno/API Keys/Set API Key Orgs.bru @@ -0,0 +1,17 @@ +meta { + name: Set API Key Orgs + type: http + seq: 7 +} + +post { + url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs + body: json + auth: inherit +} + +body:json { + { + "orgIds": ["home-lab"] + } +} diff --git a/bruno/API Keys/folder.bru b/bruno/API Keys/folder.bru new file mode 100644 index 00000000..bb8cd5c7 --- /dev/null +++ b/bruno/API Keys/folder.bru @@ -0,0 +1,3 @@ +meta { + name: API Keys +} diff --git a/bruno/Auth/login.bru b/bruno/Auth/login.bru index 3825a252..2b88066b 100644 --- a/bruno/Auth/login.bru +++ b/bruno/Auth/login.bru @@ -5,14 +5,14 @@ meta { } post { - url: http://localhost:3000/api/v1/auth/login + url: http://localhost:4000/api/v1/auth/login body: json auth: none } body:json { { - "email": "admin@fosrl.io", + "email": "owen@fossorial.io", "password": "Password123!" } } diff --git a/bruno/Auth/logout.bru b/bruno/Auth/logout.bru index 7dd134cc..623cd47f 100644 --- a/bruno/Auth/logout.bru +++ b/bruno/Auth/logout.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://localhost:3000/api/v1/auth/logout + url: http://localhost:4000/api/v1/auth/logout body: none auth: none } diff --git a/bruno/IDP/Create OIDC Provider.bru b/bruno/IDP/Create OIDC Provider.bru new file mode 100644 index 00000000..23e807cf --- /dev/null +++ b/bruno/IDP/Create OIDC Provider.bru @@ -0,0 +1,22 @@ +meta { + name: Create OIDC Provider + type: http + seq: 1 +} + +put { + url: http://localhost:3000/api/v1/org/home-lab/idp/oidc + body: json + auth: inherit +} + +body:json { + { + "clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys", + "clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3", + "authUrl": "http://localhost:9000/application/o/authorize/", + "tokenUrl": "http://localhost:9000/application/o/token/", + "scopes": ["email", "openid", "profile"], + "userIdentifier": "email" + } +} diff --git a/bruno/IDP/Generate OIDC URL.bru b/bruno/IDP/Generate OIDC URL.bru new file mode 100644 index 00000000..90443096 --- /dev/null +++ b/bruno/IDP/Generate OIDC URL.bru @@ -0,0 +1,11 @@ +meta { + name: Generate OIDC URL + type: http + seq: 2 +} + +get { + url: http://localhost:3000/api/v1 + body: none + auth: inherit +} diff --git a/bruno/IDP/folder.bru b/bruno/IDP/folder.bru new file mode 100644 index 00000000..fc136915 --- /dev/null +++ b/bruno/IDP/folder.bru @@ -0,0 +1,3 @@ +meta { + name: IDP +} diff --git a/bruno/Internal/Traefik Config.bru b/bruno/Internal/Traefik Config.bru new file mode 100644 index 00000000..9fc1c1dc --- /dev/null +++ b/bruno/Internal/Traefik Config.bru @@ -0,0 +1,11 @@ +meta { + name: Traefik Config + type: http + seq: 1 +} + +get { + url: http://localhost:3001/api/v1/traefik-config + body: none + auth: inherit +} diff --git a/bruno/Internal/folder.bru b/bruno/Internal/folder.bru new file mode 100644 index 00000000..702931ec --- /dev/null +++ b/bruno/Internal/folder.bru @@ -0,0 +1,3 @@ +meta { + name: Internal +} diff --git a/bruno/Remote Exit Node/createRemoteExitNode.bru b/bruno/Remote Exit Node/createRemoteExitNode.bru new file mode 100644 index 00000000..1c749a31 --- /dev/null +++ b/bruno/Remote Exit Node/createRemoteExitNode.bru @@ -0,0 +1,11 @@ +meta { + name: createRemoteExitNode + type: http + seq: 1 +} + +put { + url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node + body: none + auth: none +} diff --git a/bruno/Test.bru b/bruno/Test.bru new file mode 100644 index 00000000..16286ec8 --- /dev/null +++ b/bruno/Test.bru @@ -0,0 +1,11 @@ +meta { + name: Test + type: http + seq: 2 +} + +get { + url: http://localhost:3000/api/v1 + body: none + auth: inherit +} diff --git a/bruno/bruno.json b/bruno/bruno.json index f19d936a..f0ed66b3 100644 --- a/bruno/bruno.json +++ b/bruno/bruno.json @@ -1,6 +1,6 @@ { "version": "1", - "name": "Pangolin", + "name": "Pangolin Saas", "type": "collection", "ignore": [ "node_modules", diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 28097f32..21a5134f 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -20,7 +20,7 @@ services: pangolin: condition: service_healthy command: - - --reachableAt=http://gerbil:3003 + - --reachableAt=http://gerbil:3004 - --generateAndSaveKeyTo=/var/config/key - --remoteConfig=http://pangolin:3001/api/v1/ volumes: diff --git a/docker-compose.pg.yml b/docker-compose.pgr.yml similarity index 70% rename from docker-compose.pg.yml rename to docker-compose.pgr.yml index ee50d328..2a45f129 100644 --- a/docker-compose.pg.yml +++ b/docker-compose.pgr.yml @@ -11,4 +11,11 @@ services: - ./config/postgres:/var/lib/postgresql/data ports: - "5432:5432" # Map host port 5432 to container port 5432 + restart: no + + redis: + image: redis:latest # Use the latest Redis image + container_name: dev_redis # Name your Redis container + ports: + - "6379:6379" # Map host port 6379 to container port 6379 restart: no diff --git a/docker-compose.t.yml b/docker-compose.t.yml deleted file mode 100644 index 1c7716dd..00000000 --- a/docker-compose.t.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: pangolin -services: - gerbil: - image: gerbil - container_name: gerbil - network_mode: host - restart: unless-stopped - command: - - --reachableAt=http://localhost:3003 - - --generateAndSaveKeyTo=/var/config/key - - --remoteConfig=http://localhost:3001/api/v1/ - - --sni-port=443 - volumes: - - ./config/:/var/config - cap_add: - - NET_ADMIN - - SYS_MODULE - - traefik: - image: docker.io/traefik:v3.4.1 - container_name: traefik - restart: unless-stopped - network_mode: host - command: - - --configFile=/etc/traefik/traefik_config.yml - volumes: - - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - - ./certificates:/var/certificates:ro - - ./dynamic:/var/dynamic:ro - diff --git a/drizzle.pg.config.ts b/drizzle.pg.config.ts index 4d1f1e43..febd5f45 100644 --- a/drizzle.pg.config.ts +++ b/drizzle.pg.config.ts @@ -1,9 +1,13 @@ import { defineConfig } from "drizzle-kit"; import path from "path"; +const schema = [ + path.join("server", "db", "pg", "schema"), +]; + export default defineConfig({ dialect: "postgresql", - schema: [path.join("server", "db", "pg", "schema.ts")], + schema: schema, out: path.join("server", "migrations"), verbose: true, dbCredentials: { diff --git a/drizzle.sqlite.config.ts b/drizzle.sqlite.config.ts index 94574a89..4912c256 100644 --- a/drizzle.sqlite.config.ts +++ b/drizzle.sqlite.config.ts @@ -2,9 +2,13 @@ import { APP_PATH } from "@server/lib/consts"; import { defineConfig } from "drizzle-kit"; import path from "path"; +const schema = [ + path.join("server", "db", "sqlite", "schema"), +]; + export default defineConfig({ dialect: "sqlite", - schema: path.join("server", "db", "sqlite", "schema.ts"), + schema: schema, out: path.join("server", "migrations"), verbose: true, dbCredentials: { diff --git a/install/config/config.yml b/install/config/config.yml index 7d8ac865..7e73aa62 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,15 +1,10 @@ # To see all available options, please visit the docs: -# https://docs.digpangolin.com/self-host/advanced/config-file +# https://docs.digpangolin.com/ gerbil: start_port: 51820 base_endpoint: "{{.DashboardDomain}}" -{{if .HybridMode}} -managed: - id: "{{.HybridId}}" - secret: "{{.HybridSecret}}" -{{else}} app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -26,6 +21,7 @@ server: methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false + {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 97b30317..b507e914 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -6,8 +6,6 @@ services: restart: unless-stopped volumes: - ./config:/app/config - - pangolin-data:/var/certificates - - pangolin-data:/var/dynamic healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] interval: "10s" @@ -22,7 +20,7 @@ services: pangolin: condition: service_healthy command: - - --reachableAt=http://gerbil:3003 + - --reachableAt=http://gerbil:3004 - --generateAndSaveKeyTo=/var/config/key - --remoteConfig=http://pangolin:3001/api/v1/ volumes: @@ -33,7 +31,7 @@ services: ports: - 51820:51820/udp - 21820:21820/udp - - 443:{{if .HybridMode}}8443{{else}}443{{end}} + - 443:443 - 80:80 {{end}} traefik: @@ -56,15 +54,9 @@ services: - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - # Shared volume for certificates and dynamic config in file mode - - pangolin-data:/var/certificates:ro - - pangolin-data:/var/dynamic:ro networks: default: driver: bridge name: pangolin -{{if .EnableIPv6}} enable_ipv6: true{{end}} - -volumes: - pangolin-data: +{{if .EnableIPv6}} enable_ipv6: true{{end}} \ No newline at end of file diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 8bb5aa6c..a9693ce6 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -3,17 +3,12 @@ api: dashboard: true providers: -{{if not .HybridMode}} http: endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" -{{else}} - file: - directory: "/var/dynamic" - watch: true -{{end}} + experimental: plugins: badger: @@ -27,7 +22,7 @@ log: maxBackups: 3 maxAge: 3 compress: true -{{if not .HybridMode}} + certificatesResolvers: letsencrypt: acme: @@ -36,22 +31,18 @@ certificatesResolvers: email: "{{.LetsEncryptEmail}}" storage: "/letsencrypt/acme.json" caServer: "https://acme-v02.api.letsencrypt.org/directory" -{{end}} + entryPoints: web: address: ":80" websecure: address: ":443" -{{if .HybridMode}} proxyProtocol: - trustedIPs: - - 0.0.0.0/0 - - ::1/128{{end}} transport: respondingTimeouts: readTimeout: "30m" -{{if not .HybridMode}} http: + http: tls: - certResolver: "letsencrypt"{{end}} + certResolver: "letsencrypt" serversTransport: insecureSkipVerify: true diff --git a/install/get-installer.sh b/install/get-installer.sh new file mode 100644 index 00000000..d7f684ce --- /dev/null +++ b/install/get-installer.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Get installer - Cross-platform installation script +# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# GitHub repository info +REPO="fosrl/pangolin" +GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest" + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to get latest version from GitHub API +get_latest_version() { + local latest_info + + if command -v curl >/dev/null 2>&1; then + latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null) + elif command -v wget >/dev/null 2>&1; then + latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null) + else + print_error "Neither curl nor wget is available. Please install one of them." >&2 + exit 1 + fi + + if [ -z "$latest_info" ]; then + print_error "Failed to fetch latest version information" >&2 + exit 1 + fi + + # Extract version from JSON response (works without jq) + local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + + if [ -z "$version" ]; then + print_error "Could not parse version from GitHub API response" >&2 + exit 1 + fi + + # Remove 'v' prefix if present + version=$(echo "$version" | sed 's/^v//') + + echo "$version" +} + +# Detect OS and architecture +detect_platform() { + local os arch + + # Detect OS - only support Linux + case "$(uname -s)" in + Linux*) os="linux" ;; + *) + print_error "Unsupported operating system: $(uname -s). Only Linux is supported." + exit 1 + ;; + esac + + # Detect architecture - only support amd64 and arm64 + case "$(uname -m)" in + x86_64|amd64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + *) + print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux." + exit 1 + ;; + esac + + echo "${os}_${arch}" +} + +# Get installation directory +get_install_dir() { + # Install to the current directory + local install_dir="$(pwd)" + if [ ! -d "$install_dir" ]; then + print_error "Installation directory does not exist: $install_dir" + exit 1 + fi + echo "$install_dir" +} + +# Download and install installer +install_installer() { + local platform="$1" + local install_dir="$2" + local binary_name="installer_${platform}" + + local download_url="${BASE_URL}/${binary_name}" + local temp_file="/tmp/installer" + local final_path="${install_dir}/installer" + + print_status "Downloading installer from ${download_url}" + + # Download the binary + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$download_url" -o "$temp_file" + elif command -v wget >/dev/null 2>&1; then + wget -q "$download_url" -O "$temp_file" + else + print_error "Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + # Create install directory if it doesn't exist + mkdir -p "$install_dir" + + # Move binary to install directory + mv "$temp_file" "$final_path" + + # Make executable + chmod +x "$final_path" + + print_status "Installer downloaded to ${final_path}" +} + +# Verify installation +verify_installation() { + local install_dir="$1" + local installer_path="${install_dir}/installer" + + if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then + print_status "Installation successful!" + return 0 + else + print_error "Installation failed. Binary not found or not executable." + return 1 + fi +} + +# Main installation process +main() { + print_status "Installing latest version of installer..." + + # Get latest version + print_status "Fetching latest version from GitHub..." + VERSION=$(get_latest_version) + print_status "Latest version: v${VERSION}" + + # Set base URL with the fetched version + BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" + + # Detect platform + PLATFORM=$(detect_platform) + print_status "Detected platform: ${PLATFORM}" + + # Get install directory + INSTALL_DIR=$(get_install_dir) + print_status "Install directory: ${INSTALL_DIR}" + + # Install installer + install_installer "$PLATFORM" "$INSTALL_DIR" + + # Verify installation + if verify_installation "$INSTALL_DIR"; then + print_status "Installer is ready to use!" + else + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/install/go.mod b/install/go.mod index b1465ac5..8c6e06e2 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.35.0 + golang.org/x/term v0.36.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.36.0 // indirect +require golang.org/x/sys v0.37.0 // indirect diff --git a/install/go.sum b/install/go.sum index 789a291b..68e246d1 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,7 @@ -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= 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 1f7213a1..72ffbac0 100644 --- a/install/main.go +++ b/install/main.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "bytes" "embed" "fmt" "io" @@ -10,6 +9,7 @@ import ( "math/rand" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -21,9 +21,9 @@ import ( // DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD func loadVersions(config *Config) { - config.PangolinVersion = "1.9.4" - config.GerbilVersion = "1.2.1" - config.BadgerVersion = "1.2.0" + config.PangolinVersion = "replaceme" + config.GerbilVersion = "replaceme" + config.BadgerVersion = "replaceme" } //go:embed config/* @@ -47,10 +47,8 @@ type Config struct { InstallGerbil bool TraefikBouncerKey string DoCrowdsecInstall bool + EnableGeoblocking bool Secret string - HybridMode bool - HybridId string - HybridSecret string } type SupportedContainer string @@ -58,6 +56,7 @@ type SupportedContainer string const ( Docker SupportedContainer = "docker" Podman SupportedContainer = "podman" + Undefined SupportedContainer = "undefined" ) func main() { @@ -84,6 +83,7 @@ func main() { reader := bufio.NewReader(os.Stdin) var config Config + var alreadyInstalled = false // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { @@ -95,24 +95,6 @@ func main() { fmt.Println("\n=== Generating Configuration Files ===") - // If the secret and id are not generated then generate them - if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") { - // fmt.Println("Requesting hybrid credentials from cloud...") - credentials, err := requestHybridCredentials() - if err != nil { - fmt.Printf("Error requesting hybrid credentials: %v\n", err) - fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.") - os.Exit(1) - } - config.HybridId = credentials.RemoteExitNodeId - config.HybridSecret = credentials.Secret - fmt.Printf("Your managed credentials have been obtained successfully.\n") - fmt.Printf(" ID: %s\n", config.HybridId) - fmt.Printf(" Secret: %s\n", config.HybridSecret) - fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.") - readBool(reader, "Have you adopted your node?", true) - } - if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) @@ -122,6 +104,15 @@ func main() { fmt.Println("\nConfiguration files created successfully!") + // Download MaxMind database if requested + if config.EnableGeoblocking { + fmt.Println("\n=== Downloading MaxMind Database ===") + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error downloading MaxMind database: %v\n", err) + fmt.Println("You can download it manually later if needed.") + } + } + fmt.Println("\n=== Starting installation ===") if readBool(reader, "Would you like to install and start the containers?", true) { @@ -167,10 +158,36 @@ 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 { + fmt.Println("MaxMind GeoLite2 Country database found.") + if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) { + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error updating MaxMind database: %v\n", err) + fmt.Println("You can try updating it manually later if needed.") + } + } + } else { + fmt.Println("MaxMind GeoLite2 Country database not found.") + if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) { + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error downloading MaxMind database: %v\n", err) + fmt.Println("You can try downloading it manually later if needed.") + } + // Now you need to update your config file accordingly to enable geoblocking + fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n") + // add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server + fmt.Println("Add the following line under the 'server' section:") + fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"") + } + } } - if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { + if !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { @@ -190,7 +207,13 @@ func main() { return } - config.DashboardDomain = appConfig.DashboardURL + parsedURL, err := url.Parse(appConfig.DashboardURL) + if err != nil { + fmt.Printf("Error parsing URL: %v\n", err) + return + } + + config.DashboardDomain = parsedURL.Hostname() config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail config.BadgerVersion = traefikConfig.BadgerVersion @@ -205,22 +228,22 @@ func main() { } } - config.InstallationContainerType = podmanOrDocker(reader) + config.InstallationContainerType = podmanOrDocker(reader) config.DoCrowdsecInstall = true - err := installCrowdsec(config) - if (err != nil) { - fmt.Printf("Error installing CrowdSec: %v\n", err) - return - } + err := installCrowdsec(config) + if err != nil { + fmt.Printf("Error installing CrowdSec: %v\n", err) + return + } - fmt.Println("CrowdSec installed successfully!") - return + fmt.Println("CrowdSec installed successfully!") + return } } } - if !config.HybridMode { + if !alreadyInstalled { // Setup Token Section fmt.Println("\n=== Setup Token ===") @@ -241,9 +264,7 @@ func main() { fmt.Println("\nInstallation complete!") - if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) - } + fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } func podmanOrDocker(reader *bufio.Reader) SupportedContainer { @@ -318,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - for { - response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "") - if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { - config.HybridMode = true - break - } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { - config.HybridMode = false - break - } - fmt.Println("Please answer 'yes' or 'no'") + + config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) + + // Email configuration + fmt.Println("\n=== Email Configuration ===") + config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + + if config.EnableEmail { + config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") + config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) + config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") + config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? + config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - if config.HybridMode { - alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) - - if alreadyHaveCreds { - config.HybridId = readString(reader, "Enter your ID", "") - config.HybridSecret = readString(reader, "Enter your secret", "") - } - - // Try to get public IP as default - publicIP := getPublicIP() - if publicIP != "" { - fmt.Printf("Detected public IP: %s\n", publicIP) - } - config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP) - config.InstallGerbil = true - } else { - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - - // Set default dashboard domain after base domain is collected - defaultDashboardDomain := "" - if config.BaseDomain != "" { - defaultDashboardDomain = "pangolin." + config.BaseDomain - } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) - config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - - // Email configuration - fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - - if config.EnableEmail { - config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") - config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) - config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? - config.EmailNoReply = readString(reader, "Enter no-reply email address", "") - } - - // Validate required fields - if config.BaseDomain == "" { - fmt.Println("Error: Domain name is required") - os.Exit(1) - } - if config.LetsEncryptEmail == "" { - fmt.Println("Error: Let's Encrypt email is required") - os.Exit(1) - } + // Validate required fields + if config.BaseDomain == "" { + fmt.Println("Error: Domain name is required") + os.Exit(1) + } + if config.LetsEncryptEmail == "" { + fmt.Println("Error: Let's Encrypt email is required") + os.Exit(1) } // Advanced configuration @@ -385,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Advanced Configuration ===") config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) + config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false) if config.DashboardDomain == "" { fmt.Println("Error: Dashboard Domain name is required") @@ -419,11 +413,6 @@ func createConfigFiles(config Config) error { return nil } - // the hybrid does not need the dynamic config - if config.HybridMode && strings.Contains(path, "dynamic_config.yml") { - return nil - } - // skip .DS_Store if strings.Contains(path, ".DS_Store") { return nil @@ -537,12 +526,12 @@ func printSetupToken(containerType SupportedContainer, dashboardDomain string) { tokenStart := strings.Index(trimmedLine, "Token:") if tokenStart != -1 { token := strings.TrimSpace(trimmedLine[tokenStart+6:]) - fmt.Printf("Setup token: %s\n", token) - fmt.Println("") - fmt.Println("This token is required to register the first admin account in the web UI at:") - fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain) - fmt.Println("") - fmt.Println("Save this token securely. It will be invalid after the first admin is created.") + fmt.Printf("Setup token: %s\n", token) + fmt.Println("") + fmt.Println("This token is required to register the first admin account in the web UI at:") + fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain) + fmt.Println("") + fmt.Println("Save this token securely. It will be invalid after the first admin is created.") return } } @@ -556,28 +545,30 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai fmt.Println("\n=== Setup Token Instructions ===") fmt.Println("To get your setup token, you need to:") fmt.Println("") - fmt.Println("1. Start the containers:") + fmt.Println("1. Start the containers") if containerType == Docker { - fmt.Println(" docker-compose up -d") - } else { + fmt.Println(" docker compose up -d") + } else if containerType == Podman { fmt.Println(" podman-compose up -d") + } else { } fmt.Println("") fmt.Println("2. Wait for the Pangolin container to start and generate the token") fmt.Println("") - fmt.Println("3. Check the container logs for the setup token:") + fmt.Println("3. Check the container logs for the setup token") if containerType == Docker { fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") - } else { + } else if containerType == Podman { fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") + } else { } fmt.Println("") - fmt.Println("4. Look for output like:") + fmt.Println("4. Look for output like") fmt.Println(" === SETUP TOKEN GENERATED ===") fmt.Println(" Token: [your-token-here]") fmt.Println(" Use this token on the initial setup page") fmt.Println("") - fmt.Println("5. Use the token to complete initial setup at:") + fmt.Println("5. Use the token to complete initial setup at") fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain) fmt.Println("") fmt.Println("The setup token is required to register the first admin account.") @@ -634,35 +625,47 @@ func run(name string, args ...string) error { } func checkPortsAvailable(port int) error { - addr := fmt.Sprintf(":%d", port) - ln, err := net.Listen("tcp", addr) - if err != nil { - return fmt.Errorf( - "ERROR: port %d is occupied or cannot be bound: %w\n\n", - port, err, - ) - } - if closeErr := ln.Close(); closeErr != nil { - fmt.Fprintf(os.Stderr, - "WARNING: failed to close test listener on port %d: %v\n", - port, closeErr, - ) - } - return nil -} - -func checkIsPangolinInstalledWithHybrid() bool { - // Check if config/config.yml exists and contains hybrid section - if _, err := os.Stat("config/config.yml"); err != nil { - return false - } - - // Read config file to check for hybrid section - content, err := os.ReadFile("config/config.yml") + addr := fmt.Sprintf(":%d", port) + ln, err := net.Listen("tcp", addr) if err != nil { - return false + return fmt.Errorf( + "ERROR: port %d is occupied or cannot be bound: %w\n\n", + port, err, + ) } - - // Check for hybrid section - return bytes.Contains(content, []byte("managed:")) + if closeErr := ln.Close(); closeErr != nil { + fmt.Fprintf(os.Stderr, + "WARNING: failed to close test listener on port %d: %v\n", + port, closeErr, + ) + } + return nil +} + +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", + "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/install/quickStart.go b/install/quickStart.go deleted file mode 100644 index ece8e8ff..00000000 --- a/install/quickStart.go +++ /dev/null @@ -1,110 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const ( - FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" - // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" - CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" -) - -// HybridCredentials represents the response from the cloud API -type HybridCredentials struct { - RemoteExitNodeId string `json:"remoteExitNodeId"` - Secret string `json:"secret"` -} - -// APIResponse represents the full response structure from the cloud API -type APIResponse struct { - Data HybridCredentials `json:"data"` -} - -// RequestPayload represents the request body structure -type RequestPayload struct { - Token string `json:"token"` -} - -func generateValidationToken() string { - timestamp := time.Now().UnixMilli() - data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp) - obfuscated := make([]byte, len(data)) - for i, char := range []byte(data) { - obfuscated[i] = char + 5 - } - return base64.StdEncoding.EncodeToString(obfuscated) -} - -// requestHybridCredentials makes an HTTP POST request to the cloud API -// to get hybrid credentials (ID and secret) -func requestHybridCredentials() (*HybridCredentials, error) { - // Generate validation token - token := generateValidationToken() - - // Create request payload - payload := RequestPayload{ - Token: token, - } - - // Marshal payload to JSON - jsonData, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request payload: %v", err) - } - - // Create HTTP request - req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %v", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Create HTTP client with timeout - client := &http.Client{ - Timeout: 30 * time.Second, - } - - // Make the request - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - defer resp.Body.Close() - - // Check response status - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - - // Read response body for debugging - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - - // Print the raw JSON response for debugging - // fmt.Printf("Raw JSON response: %s\n", string(body)) - - // Parse response - var apiResponse APIResponse - if err := json.Unmarshal(body, &apiResponse); err != nil { - return nil, fmt.Errorf("failed to decode API response: %v", err) - } - - // Validate response data - if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" { - return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret") - } - - return &apiResponse.Data, nil -} diff --git a/messages/bg-BG.json b/messages/bg-BG.json index f617a768..01af4db4 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -18,17 +18,17 @@ "dismiss": "Отхвърляне", "componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.", "componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!", - "inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.", - "inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.", - "inviteLoginUser": "Please make sure you're logged in as the correct user.", - "inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.", - "inviteCreateUser": "Please create an account first.", - "goHome": "Go Home", - "inviteLogInOtherUser": "Log In as a Different User", - "createAnAccount": "Create an Account", - "inviteNotAccepted": "Invite Not Accepted", - "authCreateAccount": "Create an account to get started", - "authNoAccount": "Don't have an account?", + "inviteErrorNotValid": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е приета или вече не е валидна.", + "inviteErrorUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е предназначена за този потребител.", + "inviteLoginUser": "Моля, уверете се, че сте влезли като правилния потребител.", + "inviteErrorNoUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е за съществуващ потребител.", + "inviteCreateUser": "Моля, първо създайте акаунт.", + "goHome": "Отиди вкъщи", + "inviteLogInOtherUser": "Влезте като друг потребител", + "createAnAccount": "Създайте профил", + "inviteNotAccepted": "Поканата не е приета", + "authCreateAccount": "Създайте акаунт, за да започнете", + "authNoAccount": "Нямате акаунт?", "email": "Имейл", "password": "Парола", "confirmPassword": "Потвърждение на паролата", @@ -47,1477 +47,1851 @@ "edit": "Редактиране", "siteConfirmDelete": "Потвърждение на изтриване на сайта", "siteDelete": "Изтриване на сайта", - "siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.", - "siteMessageConfirm": "To confirm, please type the name of the site below.", - "siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?", - "siteManageSites": "Manage Sites", - "siteDescription": "Allow connectivity to your network through secure tunnels", - "siteCreate": "Create Site", - "siteCreateDescription2": "Follow the steps below to create and connect a new site", - "siteCreateDescription": "Create a new site to start connecting your resources", - "close": "Close", - "siteErrorCreate": "Error creating site", - "siteErrorCreateKeyPair": "Key pair or site defaults not found", - "siteErrorCreateDefaults": "Site defaults not found", - "method": "Method", - "siteMethodDescription": "This is how you will expose connections.", - "siteLearnNewt": "Learn how to install Newt on your system", - "siteSeeConfigOnce": "You will only be able to see the configuration once.", - "siteLoadWGConfig": "Loading WireGuard configuration...", - "siteDocker": "Expand for Docker Deployment Details", - "toggle": "Toggle", + "siteMessageRemove": "След изтриване, сайтът няма повече да бъде достъпен. Всички ресурси и цели, свързани със сайта, също ще бъдат премахнати.", + "siteMessageConfirm": "За потвърждение, моля, напишете името на сайта по-долу.", + "siteQuestionRemove": "Сигурни ли сте, че искате да премахнете сайта {selectedSite} от организацията?", + "siteManageSites": "Управление на сайтове", + "siteDescription": "Позволете свързване към вашата мрежа чрез сигурни тунели", + "siteCreate": "Създайте сайт", + "siteCreateDescription2": "Следвайте стъпките по-долу, за да създадете и свържете нов сайт", + "siteCreateDescription": "Създайте нов сайт, за да започнете да свързвате вашите ресурси", + "close": "Затвори", + "siteErrorCreate": "Грешка при създаване на сайт", + "siteErrorCreateKeyPair": "Ключова двойка или настройки по подразбиране на сайта не са намерени", + "siteErrorCreateDefaults": "Настройки по подразбиране на сайта не са намерени", + "method": "Метод", + "siteMethodDescription": "Това е как ще се изложат свързванията.", + "siteLearnNewt": "Научете как да инсталирате Newt на вашата система", + "siteSeeConfigOnce": "Ще можете да видите конфигурацията само веднъж.", + "siteLoadWGConfig": "Зареждане на WireGuard конфигурация...", + "siteDocker": "Разширете за детайли относно внедряване с Docker", + "toggle": "Превключване", "dockerCompose": "Docker Compose", "dockerRun": "Docker Run", - "siteLearnLocal": "Local sites do not tunnel, learn more", - "siteConfirmCopy": "I have copied the config", - "searchSitesProgress": "Search sites...", - "siteAdd": "Add Site", - "siteInstallNewt": "Install Newt", - "siteInstallNewtDescription": "Get Newt running on your system", - "WgConfiguration": "WireGuard Configuration", - "WgConfigurationDescription": "Use the following configuration to connect to your network", - "operatingSystem": "Operating System", - "commands": "Commands", - "recommended": "Recommended", - "siteNewtDescription": "For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard.", - "siteRunsInDocker": "Runs in Docker", - "siteRunsInShell": "Runs in shell on macOS, Linux, and Windows", - "siteErrorDelete": "Error deleting site", - "siteErrorUpdate": "Failed to update site", - "siteErrorUpdateDescription": "An error occurred while updating the site.", + "siteLearnLocal": "Локалните сайтове не тунелират, научете повече", + "siteConfirmCopy": "Копирах конфигурацията", + "searchSitesProgress": "Търсене на сайтове...", + "siteAdd": "Добавете сайт", + "siteInstallNewt": "Инсталирайте Newt", + "siteInstallNewtDescription": "Пуснете Newt на вашата система", + "WgConfiguration": "WireGuard конфигурация", + "WgConfigurationDescription": "Използвайте следната конфигурация, за да се свържете с вашата мрежа", + "operatingSystem": "Операционна система", + "commands": "Команди", + "recommended": "Препоръчано", + "siteNewtDescription": "За най-добро потребителско преживяване, използвайте Newt. Това е WireoGuard под повърхността и ви позволява да осъществявате достъп до личните си ресурси чрез LAN адреса им от вашия частен Pangolin дашборд.", + "siteRunsInDocker": "Работи в Docker", + "siteRunsInShell": "Работи в обвивка на macOS, Linux и Windows", + "siteErrorDelete": "Грешка при изтриване на сайта", + "siteErrorUpdate": "Неуспешно актуализиране на сайта", + "siteErrorUpdateDescription": "Възникна грешка при актуализирането на сайта.", "siteUpdated": "Сайтът е обновен", - "siteUpdatedDescription": "The site has been updated.", - "siteGeneralDescription": "Configure the general settings for this site", - "siteSettingDescription": "Configure the settings on your site", - "siteSetting": "{siteName} Settings", - "siteNewtTunnel": "Newt Tunnel (Recommended)", - "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", - "siteWg": "Basic WireGuard", - "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", - "siteLocalDescription": "Local resources only. No tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", - "siteSeeAll": "See All Sites", - "siteTunnelDescription": "Determine how you want to connect to your site", - "siteNewtCredentials": "Newt Credentials", - "siteNewtCredentialsDescription": "This is how Newt will authenticate with the server", - "siteCredentialsSave": "Save Your Credentials", - "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "siteInfo": "Site Information", - "status": "Status", - "shareTitle": "Manage Share Links", - "shareDescription": "Create shareable links to grant temporary or permanent access to your resources", - "shareSearch": "Search share links...", - "shareCreate": "Create Share Link", - "shareErrorDelete": "Failed to delete link", - "shareErrorDeleteMessage": "An error occurred deleting link", - "shareDeleted": "Link deleted", - "shareDeletedDescription": "The link has been deleted", - "shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.", - "accessToken": "Access Token", - "usageExamples": "Usage Examples", - "tokenId": "Token ID", - "requestHeades": "Request Headers", - "queryParameter": "Query Parameter", - "importantNote": "Important Note", - "shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.", - "token": "Token", - "shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.", - "shareErrorFetchResource": "Failed to fetch resources", - "shareErrorFetchResourceDescription": "An error occurred while fetching the resources", - "shareErrorCreate": "Failed to create share link", - "shareErrorCreateDescription": "An error occurred while creating the share link", - "shareCreateDescription": "Anyone with this link can access the resource", - "shareTitleOptional": "Title (optional)", - "expireIn": "Expire In", - "neverExpire": "Never expire", - "shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.", - "shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.", - "shareAccessHint": "Anyone with this link can access the resource. Share it with care.", - "shareTokenUsage": "See Access Token Usage", - "createLink": "Create Link", - "resourcesNotFound": "No resources found", - "resourceSearch": "Search resources", - "openMenu": "Open menu", - "resource": "Resource", - "title": "Title", - "created": "Created", - "expires": "Expires", - "never": "Never", - "shareErrorSelectResource": "Please select a resource", - "resourceTitle": "Manage Resources", - "resourceDescription": "Create secure proxies to your private applications", - "resourcesSearch": "Search resources...", - "resourceAdd": "Add Resource", - "resourceErrorDelte": "Error deleting resource", - "authentication": "Authentication", - "protected": "Protected", - "notProtected": "Not Protected", - "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", - "resourceMessageConfirm": "To confirm, please type the name of the resource below.", - "resourceQuestionRemove": "Are you sure you want to remove the resource {selectedResource} from the organization?", - "resourceHTTP": "HTTPS Resource", - "resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.", - "resourceRaw": "Raw TCP/UDP Resource", - "resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.", - "resourceCreate": "Create Resource", - "resourceCreateDescription": "Follow the steps below to create a new resource", - "resourceSeeAll": "See All Resources", - "resourceInfo": "Resource Information", - "resourceNameDescription": "This is the display name for the resource.", - "siteSelect": "Select site", - "siteSearch": "Search site", - "siteNotFound": "No site found.", - "siteSelectionDescription": "This site will provide connectivity to the target.", - "resourceType": "Resource Type", - "resourceTypeDescription": "Determine how you want to access your resource", - "resourceHTTPSSettings": "HTTPS Settings", - "resourceHTTPSSettingsDescription": "Configure how your resource will be accessed over HTTPS", - "domainType": "Domain Type", - "subdomain": "Subdomain", - "baseDomain": "Base Domain", - "subdomnainDescription": "The subdomain where your resource will be accessible.", - "resourceRawSettings": "TCP/UDP Settings", - "resourceRawSettingsDescription": "Configure how your resource will be accessed over TCP/UDP", - "protocol": "Protocol", - "protocolSelect": "Select a protocol", - "resourcePortNumber": "Port Number", - "resourcePortNumberDescription": "The external port number to proxy requests.", - "cancel": "Cancel", - "resourceConfig": "Configuration Snippets", - "resourceConfigDescription": "Copy and paste these configuration snippets to set up your TCP/UDP resource", - "resourceAddEntrypoints": "Traefik: Add Entrypoints", - "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", - "resourceLearnRaw": "Learn how to configure TCP/UDP resources", - "resourceBack": "Back to Resources", - "resourceGoTo": "Go to Resource", - "resourceDelete": "Delete Resource", - "resourceDeleteConfirm": "Confirm Delete Resource", - "visibility": "Visibility", - "enabled": "Enabled", - "disabled": "Disabled", - "general": "General", - "generalSettings": "General Settings", - "proxy": "Proxy", - "internal": "Internal", - "rules": "Rules", - "resourceSettingDescription": "Configure the settings on your resource", - "resourceSetting": "{resourceName} Settings", - "alwaysAllow": "Always Allow", - "alwaysDeny": "Always Deny", - "passToAuth": "Pass to Auth", - "orgSettingsDescription": "Configure your organization's general settings", - "orgGeneralSettings": "Organization Settings", - "orgGeneralSettingsDescription": "Manage your organization details and configuration", - "saveGeneralSettings": "Save General Settings", - "saveSettings": "Save Settings", - "orgDangerZone": "Danger Zone", - "orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.", - "orgDelete": "Delete Organization", - "orgDeleteConfirm": "Confirm Delete Organization", - "orgMessageRemove": "This action is irreversible and will delete all associated data.", - "orgMessageConfirm": "To confirm, please type the name of the organization below.", - "orgQuestionRemove": "Are you sure you want to remove the organization {selectedOrg}?", - "orgUpdated": "Organization updated", - "orgUpdatedDescription": "The organization has been updated.", - "orgErrorUpdate": "Failed to update organization", - "orgErrorUpdateMessage": "An error occurred while updating the organization.", - "orgErrorFetch": "Failed to fetch organizations", - "orgErrorFetchMessage": "An error occurred while listing your organizations", - "orgErrorDelete": "Failed to delete organization", - "orgErrorDeleteMessage": "An error occurred while deleting the organization.", - "orgDeleted": "Organization deleted", - "orgDeletedMessage": "The organization and its data has been deleted.", - "orgMissing": "Organization ID Missing", - "orgMissingMessage": "Unable to regenerate invitation without an organization ID.", - "accessUsersManage": "Manage Users", - "accessUsersDescription": "Invite users and add them to roles to manage access to your organization", - "accessUsersSearch": "Search users...", - "accessUserCreate": "Create User", - "accessUserRemove": "Remove User", - "username": "Username", - "identityProvider": "Identity Provider", - "role": "Role", - "nameRequired": "Name is required", - "accessRolesManage": "Manage Roles", - "accessRolesDescription": "Configure roles to manage access to your organization", - "accessRolesSearch": "Search roles...", - "accessRolesAdd": "Add Role", - "accessRoleDelete": "Delete Role", - "description": "Description", - "inviteTitle": "Open Invitations", - "inviteDescription": "Manage your invitations to other users", - "inviteSearch": "Search invitations...", - "minutes": "Minutes", - "hours": "Hours", - "days": "Days", - "weeks": "Weeks", - "months": "Months", - "years": "Years", - "day": "{count, plural, one {# day} other {# days}}", - "apiKeysTitle": "API Key Information", - "apiKeysConfirmCopy2": "You must confirm that you have copied the API key.", - "apiKeysErrorCreate": "Error creating API key", - "apiKeysErrorSetPermission": "Error setting permissions", - "apiKeysCreate": "Generate API Key", - "apiKeysCreateDescription": "Generate a new API key for your organization", - "apiKeysGeneralSettings": "Permissions", - "apiKeysGeneralSettingsDescription": "Determine what this API key can do", - "apiKeysList": "Your API Key", - "apiKeysSave": "Save Your API Key", - "apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "apiKeysInfo": "Your API key is:", - "apiKeysConfirmCopy": "I have copied the API key", - "generate": "Generate", - "done": "Done", - "apiKeysSeeAll": "See All API Keys", - "apiKeysPermissionsErrorLoadingActions": "Error loading API key actions", - "apiKeysPermissionsErrorUpdate": "Error setting permissions", - "apiKeysPermissionsUpdated": "Permissions updated", - "apiKeysPermissionsUpdatedDescription": "The permissions have been updated.", - "apiKeysPermissionsGeneralSettings": "Permissions", - "apiKeysPermissionsGeneralSettingsDescription": "Determine what this API key can do", - "apiKeysPermissionsSave": "Save Permissions", - "apiKeysPermissionsTitle": "Permissions", - "apiKeys": "API Keys", - "searchApiKeys": "Search API keys...", - "apiKeysAdd": "Generate API Key", - "apiKeysErrorDelete": "Error deleting API key", - "apiKeysErrorDeleteMessage": "Error deleting API key", - "apiKeysQuestionRemove": "Are you sure you want to remove the API key {selectedApiKey} from the organization?", - "apiKeysMessageRemove": "Once removed, the API key will no longer be able to be used.", - "apiKeysMessageConfirm": "To confirm, please type the name of the API key below.", - "apiKeysDeleteConfirm": "Confirm Delete API Key", - "apiKeysDelete": "Delete API Key", - "apiKeysManage": "Manage API Keys", - "apiKeysDescription": "API keys are used to authenticate with the integration API", - "apiKeysSettings": "{apiKeyName} Settings", - "userTitle": "Manage All Users", - "userDescription": "View and manage all users in the system", - "userAbount": "About User Management", - "userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.", - "userServer": "Server Users", - "userSearch": "Search server users...", - "userErrorDelete": "Error deleting user", - "userDeleteConfirm": "Confirm Delete User", - "userDeleteServer": "Delete User from Server", - "userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.", - "userMessageConfirm": "To confirm, please type the name of the user below.", - "userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?", - "licenseKey": "License Key", - "valid": "Valid", - "numberOfSites": "Number of Sites", - "licenseKeySearch": "Search license keys...", - "licenseKeyAdd": "Add License Key", - "type": "Type", - "licenseKeyRequired": "License key is required", - "licenseTermsAgree": "You must agree to the license terms", - "licenseErrorKeyLoad": "Failed to load license keys", - "licenseErrorKeyLoadDescription": "An error occurred loading license keys.", - "licenseErrorKeyDelete": "Failed to delete license key", - "licenseErrorKeyDeleteDescription": "An error occurred deleting license key.", - "licenseKeyDeleted": "License key deleted", - "licenseKeyDeletedDescription": "The license key has been deleted.", - "licenseErrorKeyActivate": "Failed to activate license key", - "licenseErrorKeyActivateDescription": "An error occurred while activating the license key.", - "licenseAbout": "About Licensing", - "communityEdition": "Community Edition", - "licenseAboutDescription": "This is for business and enterprise users who are using Pangolin in a commercial environment. If you are using Pangolin for personal use, you can ignore this section.", - "licenseKeyActivated": "License key activated", - "licenseKeyActivatedDescription": "The license key has been successfully activated.", - "licenseErrorKeyRecheck": "Failed to recheck license keys", - "licenseErrorKeyRecheckDescription": "An error occurred rechecking license keys.", - "licenseErrorKeyRechecked": "License keys rechecked", - "licenseErrorKeyRecheckedDescription": "All license keys have been rechecked", - "licenseActivateKey": "Activate License Key", - "licenseActivateKeyDescription": "Enter a license key to activate it.", - "licenseActivate": "Activate License", - "licenseAgreement": "By checking this box, you confirm that you have read and agree to the license terms corresponding to the tier associated with your license key.", - "fossorialLicense": "View Fossorial Commercial License & Subscription Terms", - "licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.", - "licenseMessageConfirm": "To confirm, please type the license key below.", - "licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?", - "licenseKeyDelete": "Delete License Key", - "licenseKeyDeleteConfirm": "Confirm Delete License Key", - "licenseTitle": "Manage License Status", - "licenseTitleDescription": "View and manage license keys in the system", - "licenseHost": "Host License", - "licenseHostDescription": "Manage the main license key for the host.", - "licensedNot": "Not Licensed", - "hostId": "Host ID", - "licenseReckeckAll": "Recheck All Keys", - "licenseSiteUsage": "Sites Usage", - "licenseSiteUsageDecsription": "View the number of sites using this license.", - "licenseNoSiteLimit": "There is no limit on the number of sites using an unlicensed host.", - "licensePurchase": "Purchase License", - "licensePurchaseSites": "Purchase Additional Sites", - "licenseSitesUsedMax": "{usedSites} of {maxSites} sites used", - "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} in system.", - "licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}", - "licenseFee": "License fee", - "licensePriceSite": "Price per site", - "total": "Total", - "licenseContinuePayment": "Continue to Payment", - "pricingPage": "pricing page", - "pricingPortal": "See Purchase Portal", - "licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ", - "invite": "Invitations", - "inviteRegenerate": "Regenerate Invitation", - "inviteRegenerateDescription": "Revoke previous invitation and create a new one", - "inviteRemove": "Remove Invitation", - "inviteRemoveError": "Failed to remove invitation", - "inviteRemoveErrorDescription": "An error occurred while removing the invitation.", - "inviteRemoved": "Invitation removed", - "inviteRemovedDescription": "The invitation for {email} has been removed.", - "inviteQuestionRemove": "Are you sure you want to remove the invitation {email}?", - "inviteMessageRemove": "Once removed, this invitation will no longer be valid. You can always re-invite the user later.", - "inviteMessageConfirm": "To confirm, please type the email address of the invitation below.", - "inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for {email}? This will revoke the previous invitation.", - "inviteRemoveConfirm": "Confirm Remove Invitation", - "inviteRegenerated": "Invitation Regenerated", - "inviteSent": "A new invitation has been sent to {email}.", - "inviteSentEmail": "Send email notification to the user", - "inviteGenerate": "A new invitation has been generated for {email}.", - "inviteDuplicateError": "Duplicate Invite", - "inviteDuplicateErrorDescription": "An invitation for this user already exists.", - "inviteRateLimitError": "Rate Limit Exceeded", - "inviteRateLimitErrorDescription": "You have exceeded the limit of 3 regenerations per hour. Please try again later.", - "inviteRegenerateError": "Failed to Regenerate Invitation", - "inviteRegenerateErrorDescription": "An error occurred while regenerating the invitation.", - "inviteValidityPeriod": "Validity Period", - "inviteValidityPeriodSelect": "Select validity period", - "inviteRegenerateMessage": "The invitation has been regenerated. The user must access the link below to accept the invitation.", - "inviteRegenerateButton": "Regenerate", - "expiresAt": "Expires At", - "accessRoleUnknown": "Unknown Role", - "placeholder": "Placeholder", - "userErrorOrgRemove": "Failed to remove user", - "userErrorOrgRemoveDescription": "An error occurred while removing the user.", - "userOrgRemoved": "User removed", - "userOrgRemovedDescription": "The user {email} has been removed from the organization.", - "userQuestionOrgRemove": "Are you sure you want to remove {email} from the organization?", - "userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.", - "userMessageOrgConfirm": "To confirm, please type the name of the of the user below.", - "userRemoveOrgConfirm": "Confirm Remove User", - "userRemoveOrg": "Remove User from Organization", - "users": "Users", - "accessRoleMember": "Member", - "accessRoleOwner": "Owner", - "userConfirmed": "Confirmed", - "idpNameInternal": "Internal", - "emailInvalid": "Invalid email address", - "inviteValidityDuration": "Please select a duration", - "accessRoleSelectPlease": "Please select a role", - "usernameRequired": "Username is required", - "idpSelectPlease": "Please select an identity provider", - "idpGenericOidc": "Generic OAuth2/OIDC provider.", - "accessRoleErrorFetch": "Failed to fetch roles", - "accessRoleErrorFetchDescription": "An error occurred while fetching the roles", - "idpErrorFetch": "Failed to fetch identity providers", - "idpErrorFetchDescription": "An error occurred while fetching identity providers", - "userErrorExists": "User Already Exists", - "userErrorExistsDescription": "This user is already a member of the organization.", - "inviteError": "Failed to invite user", - "inviteErrorDescription": "An error occurred while inviting the user", - "userInvited": "User invited", - "userInvitedDescription": "The user has been successfully invited.", - "userErrorCreate": "Failed to create user", - "userErrorCreateDescription": "An error occurred while creating the user", - "userCreated": "User created", - "userCreatedDescription": "The user has been successfully created.", - "userTypeInternal": "Internal User", - "userTypeInternalDescription": "Invite a user to join your organization directly.", - "userTypeExternal": "External User", - "userTypeExternalDescription": "Create a user with an external identity provider.", - "accessUserCreateDescription": "Follow the steps below to create a new user", - "userSeeAll": "See All Users", - "userTypeTitle": "User Type", - "userTypeDescription": "Determine how you want to create the user", - "userSettings": "User Information", - "userSettingsDescription": "Enter the details for the new user", - "inviteEmailSent": "Send invite email to user", - "inviteValid": "Valid For", - "selectDuration": "Select duration", - "accessRoleSelect": "Select role", - "inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.", - "inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.", - "inviteExpiresIn": "The invite will expire in {days, plural, one {# day} other {# days}}.", - "idpTitle": "Identity Provider", - "idpSelect": "Select the identity provider for the external user", - "idpNotConfigured": "No identity providers are configured. Please configure an identity provider before creating external users.", - "usernameUniq": "This must match the unique username that exists in the selected identity provider.", - "emailOptional": "Email (Optional)", - "nameOptional": "Name (Optional)", - "accessControls": "Access Controls", - "userDescription2": "Manage the settings on this user", - "accessRoleErrorAdd": "Failed to add user to role", - "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", - "userSaved": "User saved", - "userSavedDescription": "The user has been updated.", - "autoProvisioned": "Auto Provisioned", - "autoProvisionedDescription": "Allow this user to be automatically managed by identity provider", - "accessControlsDescription": "Manage what this user can access and do in the organization", - "accessControlsSubmit": "Save Access Controls", - "roles": "Roles", - "accessUsersRoles": "Manage Users & Roles", - "accessUsersRolesDescription": "Invite users and add them to roles to manage access to your organization", - "key": "Key", - "createdAt": "Created At", - "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", - "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - "proxyEnableSSL": "Enable SSL (https)", - "targetErrorFetch": "Failed to fetch targets", - "targetErrorFetchDescription": "An error occurred while fetching targets", - "siteErrorFetch": "Failed to fetch resource", - "siteErrorFetchDescription": "An error occurred while fetching resource", - "targetErrorDuplicate": "Duplicate target", - "targetErrorDuplicateDescription": "A target with these settings already exists", - "targetWireGuardErrorInvalidIp": "Invalid target IP", - "targetWireGuardErrorInvalidIpDescription": "Target IP must be within the site subnet", - "targetsUpdated": "Targets updated", - "targetsUpdatedDescription": "Targets and settings updated successfully", - "targetsErrorUpdate": "Failed to update targets", - "targetsErrorUpdateDescription": "An error occurred while updating targets", - "targetTlsUpdate": "TLS settings updated", - "targetTlsUpdateDescription": "Your TLS settings have been updated successfully", - "targetErrorTlsUpdate": "Failed to update TLS settings", - "targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings", - "proxyUpdated": "Proxy settings updated", - "proxyUpdatedDescription": "Your proxy settings have been updated successfully", - "proxyErrorUpdate": "Failed to update proxy settings", - "proxyErrorUpdateDescription": "An error occurred while updating proxy settings", - "targetAddr": "IP / Hostname", - "targetPort": "Port", - "targetProtocol": "Protocol", - "targetTlsSettings": "Secure Connection Configuration", - "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", - "targetTlsSettingsAdvanced": "Advanced TLS Settings", - "targetTlsSni": "TLS Server Name (SNI)", - "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", - "targetTlsSubmit": "Save Settings", - "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your backend services", - "targetStickySessions": "Enable Sticky Sessions", - "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", - "methodSelect": "Select method", - "targetSubmit": "Add Target", - "targetNoOne": "No targets. Add a target using the form.", - "targetNoOneDescription": "Adding more than one target above will enable load balancing.", - "targetsSubmit": "Save Targets", - "proxyAdditional": "Additional Proxy Settings", - "proxyAdditionalDescription": "Configure how your resource handles proxy settings", - "proxyCustomHeader": "Custom Host Header", - "proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.", - "proxyAdditionalSubmit": "Save Proxy Settings", - "subnetMaskErrorInvalid": "Invalid subnet mask. Must be between 0 and 32.", - "ipAddressErrorInvalidFormat": "Invalid IP address format", - "ipAddressErrorInvalidOctet": "Invalid IP address octet", - "path": "Path", - "matchPath": "Match Path", - "ipAddressRange": "IP Range", - "rulesErrorFetch": "Failed to fetch rules", - "rulesErrorFetchDescription": "An error occurred while fetching rules", - "rulesErrorDuplicate": "Duplicate rule", - "rulesErrorDuplicateDescription": "A rule with these settings already exists", - "rulesErrorInvalidIpAddressRange": "Invalid CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Please enter a valid CIDR value", - "rulesErrorInvalidUrl": "Invalid URL path", - "rulesErrorInvalidUrlDescription": "Please enter a valid URL path value", - "rulesErrorInvalidIpAddress": "Invalid IP", - "rulesErrorInvalidIpAddressDescription": "Please enter a valid IP address", - "rulesErrorUpdate": "Failed to update rules", - "rulesErrorUpdateDescription": "An error occurred while updating rules", - "rulesUpdated": "Enable Rules", - "rulesUpdatedDescription": "Rule evaluation has been updated", - "rulesMatchIpAddressRangeDescription": "Enter an address in CIDR format (e.g., 103.21.244.0/22)", - "rulesMatchIpAddress": "Enter an IP address (e.g., 103.21.244.12)", - "rulesMatchUrl": "Enter a URL path or pattern (e.g., /api/v1/todos or /api/v1/*)", - "rulesErrorInvalidPriority": "Invalid Priority", - "rulesErrorInvalidPriorityDescription": "Please enter a valid priority", - "rulesErrorDuplicatePriority": "Duplicate Priorities", - "rulesErrorDuplicatePriorityDescription": "Please enter unique priorities", - "ruleUpdated": "Rules updated", - "ruleUpdatedDescription": "Rules updated successfully", - "ruleErrorUpdate": "Operation failed", - "ruleErrorUpdateDescription": "An error occurred during the save operation", - "rulesPriority": "Priority", - "rulesAction": "Action", - "rulesMatchType": "Match Type", - "value": "Value", - "rulesAbout": "About Rules", - "rulesAboutDescription": "Rules allow you to control access to your resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.", - "rulesActions": "Actions", - "rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods", - "rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted", - "rulesActionPassToAuth": "Pass to Auth: Allow authentication methods to be attempted", - "rulesMatchCriteria": "Matching Criteria", - "rulesMatchCriteriaIpAddress": "Match a specific IP address", - "rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation", - "rulesMatchCriteriaUrl": "Match a URL path or pattern", - "rulesEnable": "Enable Rules", - "rulesEnableDescription": "Enable or disable rule evaluation for this resource", - "rulesResource": "Resource Rules Configuration", - "rulesResourceDescription": "Configure rules to control access to your resource", - "ruleSubmit": "Add Rule", - "rulesNoOne": "No rules. Add a rule using the form.", - "rulesOrder": "Rules are evaluated by priority in ascending order.", - "rulesSubmit": "Save Rules", - "resourceErrorCreate": "Error creating resource", - "resourceErrorCreateDescription": "An error occurred when creating the resource", - "resourceErrorCreateMessage": "Error creating resource:", - "resourceErrorCreateMessageDescription": "An unexpected error occurred", - "sitesErrorFetch": "Error fetching sites", - "sitesErrorFetchDescription": "An error occurred when fetching the sites", - "domainsErrorFetch": "Error fetching domains", - "domainsErrorFetchDescription": "An error occurred when fetching the domains", - "none": "None", - "unknown": "Unknown", - "resources": "Resources", - "resourcesDescription": "Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.", - "resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption", - "resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods", - "resourcesUsersRolesAccess": "User and role-based access control", - "resourcesErrorUpdate": "Failed to toggle resource", - "resourcesErrorUpdateDescription": "An error occurred while updating the resource", - "access": "Access", - "shareLink": "{resource} Share Link", - "resourceSelect": "Select resource", - "shareLinks": "Share Links", - "share": "Shareable Links", - "shareDescription2": "Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.", - "shareEasyCreate": "Easy to create and share", - "shareConfigurableExpirationDuration": "Configurable expiration duration", - "shareSecureAndRevocable": "Secure and revocable", - "nameMin": "Name must be at least {len} characters.", - "nameMax": "Name must not be longer than {len} characters.", - "sitesConfirmCopy": "Please confirm that you have copied the config.", - "unknownCommand": "Unknown command", - "newtErrorFetchReleases": "Failed to fetch release info: {err}", - "newtErrorFetchLatest": "Error fetching latest release: {err}", - "newtEndpoint": "Newt Endpoint", + "siteUpdatedDescription": "Сайтът е актуализиран.", + "siteGeneralDescription": "Конфигурирайте общи настройки за този сайт", + "siteSettingDescription": "Настройте настройките на вашия сайт", + "siteSetting": "Настройки на {siteName}", + "siteNewtTunnel": "Newt тунел (Препоръчително)", + "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": "Local resources only. No tunneling. Only available on remote nodes.", + "siteSeeAll": "Вижте всички сайтове", + "siteTunnelDescription": "Определете как искате да се свържете с вашия сайт", + "siteNewtCredentials": "Newt Удостоверения", + "siteNewtCredentialsDescription": "Това е така, защото Newt ще се удостовери със сървъра", + "siteCredentialsSave": "Запазете вашите удостоверения", + "siteCredentialsSaveDescription": "Ще можете да виждате това само веднъж. Уверете се да го копирате на сигурно място.", + "siteInfo": "Информация за сайта", + "status": "Статус", + "shareTitle": "Управление на връзки за споделяне", + "shareDescription": "Създайте споделяеми връзки, за да разрешите временен или постоянен достъп до вашите ресурси", + "shareSearch": "Търсене на връзки за споделяне...", + "shareCreate": "Създайте връзка за споделяне", + "shareErrorDelete": "Неуспешно изтриване на връзката", + "shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката", + "shareDeleted": "Връзката беше изтрита", + "shareDeletedDescription": "Връзката беше премахната", + "shareTokenDescription": "Вашият достъп токен може да се предава по два начина: като параметър на URL или в заглавките на заявката. Тези трябва да се предават от клиента при всяка заявка за удостоверен достъп.", + "accessToken": "Достъп Токен", + "usageExamples": "Примери за използване", + "tokenId": "Токен ID", + "requestHeades": "Заглавие на заявката", + "queryParameter": "Параметър за заявка", + "importantNote": "Важно бележка", + "shareImportantDescription": "По съображения за сигурност, използването на заглавки се препоръчва пред параметри на заявка, когато е възможно, тъй като параметри на заявка могат да бъдат записвани в логове на сървъра или в историята на браузъра.", + "token": "Токен", + "shareTokenSecurety": "Пазете вашият достъп токен в безопасност. Не го споделяйте в публичнодостъпни зони или клиентски код.", + "shareErrorFetchResource": "Неуспешно вземане на ресурси", + "shareErrorFetchResourceDescription": "Възникна грешка при опит за вземане на ресурсите", + "shareErrorCreate": "Неуспешно създаване на връзка за споделяне", + "shareErrorCreateDescription": "Възникна грешка при създаването на връзката за споделяне", + "shareCreateDescription": "Всеки с тази връзка може да получи достъп до ресурса", + "shareTitleOptional": "Заглавие (по избор)", + "expireIn": "Изтече", + "neverExpire": "Никога не изтича", + "shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.", + "shareSeeOnce": "Ще можете да видите тази връзка само веднъж. Уверете се да я копирате.", + "shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.", + "shareTokenUsage": "Вижте използването на токена за достъп", + "createLink": "Създаване на връзка", + "resourcesNotFound": "Не са намерени ресурси", + "resourceSearch": "Търсене на ресурси", + "openMenu": "Отваряне на менюто", + "resource": "Ресурс", + "title": "Заглавие", + "created": "Създадено", + "expires": "Изтича", + "never": "Никога", + "shareErrorSelectResource": "Моля, изберете ресурс", + "resourceTitle": "Управление на ресурси", + "resourceDescription": "Създайте сигурни проксита към вашите частни приложения", + "resourcesSearch": "Търсене на ресурси...", + "resourceAdd": "Добавете ресурс", + "resourceErrorDelte": "Грешка при изтриване на ресурс", + "authentication": "Удостоверяване", + "protected": "Защита", + "notProtected": "Не защитен", + "resourceMessageRemove": "След като се премахне, ресурсът няма повече да бъде достъпен. Всички цели, свързани с ресурса, също ще бъдат премахнати.", + "resourceMessageConfirm": "За потвърждение, моля, напишете името на ресурса по-долу.", + "resourceQuestionRemove": "Сигурни ли сте, че искате да премахнете ресурса {selectedResource} от организацията?", + "resourceHTTP": "HTTPS ресурс", + "resourceHTTPDescription": "Прокси заявки към вашето приложение през HTTPS с помощта на субдомейн или базов домейн.", + "resourceRaw": "Суров TCP/UDP ресурс", + "resourceRawDescription": "Прокси заявки към вашето приложение през TCP/UDP с помощта на номер на порт.", + "resourceCreate": "Създайте ресурс", + "resourceCreateDescription": "Следвайте стъпките по-долу, за да създадете нов ресурс", + "resourceSeeAll": "Вижте всички ресурси", + "resourceInfo": "Информация за ресурса", + "resourceNameDescription": "Това е дисплейното име на ресурса.", + "siteSelect": "Изберете сайт", + "siteSearch": "Търсене на сайт", + "siteNotFound": "Няма намерени сайтове.", + "selectCountry": "Изберете държава", + "searchCountries": "Търсене на държави...", + "noCountryFound": "Не е намерена държава.", + "siteSelectionDescription": "Този сайт ще осигури свързаност до целта.", + "resourceType": "Тип ресурс", + "resourceTypeDescription": "Определете как искате да получите достъп до вашия ресурс", + "resourceHTTPSSettings": "HTTPS настройки", + "resourceHTTPSSettingsDescription": "Конфигурирайте как вашият ресурс ще бъде достъпен през HTTPS", + "domainType": "Тип домейн", + "subdomain": "Субдомейн", + "baseDomain": "Базов домейн", + "subdomnainDescription": "Субдомейнът, в който ще бъде достъпен вашият ресурс.", + "resourceRawSettings": "TCP/UDP настройки", + "resourceRawSettingsDescription": "Конфигурирайте как вашият ресурс ще бъде достъпен през TCP/UDP", + "protocol": "Протокол", + "protocolSelect": "Изберете протокол", + "resourcePortNumber": "Номер на порт", + "resourcePortNumberDescription": "Външен номер на порт за прокси заявки.", + "cancel": "Отмяна", + "resourceConfig": "Конфигурационни фрагменти", + "resourceConfigDescription": "Копирайте и поставете тези конфигурационни фрагменти за настройка на вашия TCP/UDP ресурс", + "resourceAddEntrypoints": "Traefik: Добавете Входни точки", + "resourceExposePorts": "Gerbil: Изложете портове в Docker Compose", + "resourceLearnRaw": "Научете как да конфигурирате TCP/UDP ресурси", + "resourceBack": "Назад към ресурсите", + "resourceGoTo": "Отидете към ресурса", + "resourceDelete": "Изтрийте ресурс", + "resourceDeleteConfirm": "Потвърдете изтриване на ресурс", + "visibility": "Видимост", + "enabled": "Активиран", + "disabled": "Деактивиран", + "general": "Общи", + "generalSettings": "Общи настройки", + "proxy": "Прокси", + "internal": "Вътрешен", + "rules": "Правила", + "resourceSettingDescription": "Конфигурирайте настройките на вашия ресурс", + "resourceSetting": "Настройки на {resourceName}", + "alwaysAllow": "Винаги позволявай", + "alwaysDeny": "Винаги отказвай", + "passToAuth": "Прехвърляне към удостоверяване", + "orgSettingsDescription": "Конфигурирайте общите настройки на вашата организация", + "orgGeneralSettings": "Настройки на организацията", + "orgGeneralSettingsDescription": "Управлявайте детайлите и конфигурацията на вашата организация", + "saveGeneralSettings": "Запазете общите настройки", + "saveSettings": "Запазване на настройките", + "orgDangerZone": "Опасна зона", + "orgDangerZoneDescription": "След като изтриете тази организация, няма връщане назад. Моля, бъдете сигурен.", + "orgDelete": "Изтрийте организацията", + "orgDeleteConfirm": "Потвърдете изтриване на организация", + "orgMessageRemove": "Това действие е необратимо и ще изтрие всички свързани данни.", + "orgMessageConfirm": "За потвърждение, моля, напишете името на организацията по-долу.", + "orgQuestionRemove": "Сигурни ли сте, че искате да премахнете организацията {selectedOrg}?", + "orgUpdated": "Организацията е актуализирана", + "orgUpdatedDescription": "Организацията е обновена.", + "orgErrorUpdate": "Неуспешно актуализиране на организацията", + "orgErrorUpdateMessage": "Възникна грешка при актуализиране на организацията.", + "orgErrorFetch": "Неуспешно вземане на организации", + "orgErrorFetchMessage": "Възникна грешка при изброяване на вашите организации", + "orgErrorDelete": "Неуспешно изтриване на организацията", + "orgErrorDeleteMessage": "Възникна грешка при изтриването на организацията.", + "orgDeleted": "Организацията е изтрита", + "orgDeletedMessage": "Организацията и нейните данни са изтрити.", + "orgMissing": "Липсва идентификатор на организация", + "orgMissingMessage": "Невъзможност за регенериране на покана без идентификатор на организация.", + "accessUsersManage": "Управление на потребители", + "accessUsersDescription": "Поканете потребители и ги добавете в роли, за да управлявате достъпа до вашата организация", + "accessUsersSearch": "Търсене на потребители...", + "accessUserCreate": "Създайте потребител", + "accessUserRemove": "Премахнете потребител", + "username": "Потребителско име", + "identityProvider": "Доставчик на идентичност", + "role": "Роля", + "nameRequired": "Името е задължително", + "accessRolesManage": "Управление на роли", + "accessRolesDescription": "Конфигурирайте роли, за да управлявате достъпа до вашата организация", + "accessRolesSearch": "Търсене на роли...", + "accessRolesAdd": "Добавете роля", + "accessRoleDelete": "Изтриване на роля", + "description": "Описание", + "inviteTitle": "Отворени покани", + "inviteDescription": "Управление на вашите покани към други потребители", + "inviteSearch": "Търсене на покани...", + "minutes": "Минути", + "hours": "Часове", + "days": "Дни", + "weeks": "Седмици", + "months": "Месеци", + "years": "Години", + "day": "{count, plural, one {# ден} other {# дни}}", + "apiKeysTitle": "Информация за API ключ", + "apiKeysConfirmCopy2": "Трябва да потвърдите, че сте копирали API ключът.", + "apiKeysErrorCreate": "Грешка при създаване на API ключ", + "apiKeysErrorSetPermission": "Грешка при задаване на разрешения", + "apiKeysCreate": "Генерирайте API ключ", + "apiKeysCreateDescription": "Генерирайте нов API ключ за вашата организация", + "apiKeysGeneralSettings": "Разрешения", + "apiKeysGeneralSettingsDescription": "Определете какво може да прави този API ключ", + "apiKeysList": "Вашият API ключ", + "apiKeysSave": "Запазване на вашия API ключ", + "apiKeysSaveDescription": "Ще можете да виждате това само веднъж. Уверете се да го копирате на сигурно място.", + "apiKeysInfo": "Вашият API ключ е:", + "apiKeysConfirmCopy": "Копирах API ключа", + "generate": "Генериране", + "done": "Готово", + "apiKeysSeeAll": "Вижте всички API ключове", + "apiKeysPermissionsErrorLoadingActions": "Грешка при зареждане на действията на API ключа", + "apiKeysPermissionsErrorUpdate": "Грешка при задаване на разрешения", + "apiKeysPermissionsUpdated": "Разрешенията са актуализирани", + "apiKeysPermissionsUpdatedDescription": "Разрешенията са обновени.", + "apiKeysPermissionsGeneralSettings": "Разрешения", + "apiKeysPermissionsGeneralSettingsDescription": "Определете какво може да прави този API ключ", + "apiKeysPermissionsSave": "Запазете разрешенията", + "apiKeysPermissionsTitle": "Разрешения", + "apiKeys": "API ключове", + "searchApiKeys": "Търсене на API ключове...", + "apiKeysAdd": "Генерирайте API ключ", + "apiKeysErrorDelete": "Грешка при изтриване на API ключ", + "apiKeysErrorDeleteMessage": "Грешка при изтриване на API ключ", + "apiKeysQuestionRemove": "Сигурни ли сте, че искате да премахнете API ключа {selectedApiKey} от организацията?", + "apiKeysMessageRemove": "След като бъде премахнат, API ключът няма вече да може да се използва.", + "apiKeysMessageConfirm": "За потвърждение, моля, напишете името на API ключа по-долу.", + "apiKeysDeleteConfirm": "Потвърдете изтриване на API ключ", + "apiKeysDelete": "Изтрийте API ключа", + "apiKeysManage": "Управление на API ключове", + "apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API", + "apiKeysSettings": "Настройки на {apiKeyName}", + "userTitle": "Управление на всички потребители", + "userDescription": "Преглед и управление на всички потребители в системата", + "userAbount": "Относно управлението на потребители", + "userAbountDescription": "Тази таблица показва всички рут потребителски обекти в системата. Всеки потребител може да принадлежи към множество организации. Изтриването на потребител от организация не премахва неговия рут потребителски обект - той ще остане в системата. За да премахнете напълно потребител от системата, трябва да изтриете неговия рут потребителски обект чрез действие за изтриване в тази таблица.", + "userServer": "Сървърни потребители", + "userSearch": "Търсене на сървърни потребители...", + "userErrorDelete": "Грешка при изтриване на потребител", + "userDeleteConfirm": "Потвърдете изтриването на потребител", + "userDeleteServer": "Изтрийте потребителя от сървъра", + "userMessageRemove": "Потребителят ще бъде премахнат от всички организации и напълно заличен от сървъра.", + "userMessageConfirm": "За да потвърдите, въведете името на потребителя по-долу.", + "userQuestionRemove": "Сигурни ли сте, че искате да изтриете завинаги {selectedUser} от сървъра?", + "licenseKey": "Ключ за лиценз", + "valid": "Валиден", + "numberOfSites": "Брой сайтове", + "licenseKeySearch": "Търсене на лицензионни ключове...", + "licenseKeyAdd": "Добавете лицензионен ключ", + "type": "Тип", + "licenseKeyRequired": "Необходим е лицензионен ключ", + "licenseTermsAgree": "Трябва да се съгласите с лицензионните условия", + "licenseErrorKeyLoad": "Неуспешно зареждане на лицензионни ключове", + "licenseErrorKeyLoadDescription": "Възникна грешка при зареждане на лицензионните ключове.", + "licenseErrorKeyDelete": "Неуспешно изтриване на лицензионен ключ", + "licenseErrorKeyDeleteDescription": "Възникна грешка при изтриване на лицензионния ключ.", + "licenseKeyDeleted": "Лицензионният ключ е изтрит", + "licenseKeyDeletedDescription": "Лицензионният ключ беше изтрит.", + "licenseErrorKeyActivate": "Неуспешно активиране на лицензионния ключ", + "licenseErrorKeyActivateDescription": "Възникна грешка при активирането на лицензионния ключ.", + "licenseAbout": "Относно лицензите", + "communityEdition": "Комюнити издание", + "licenseAboutDescription": "Това е за бизнес и корпоративни потребители, които използват Pangolin в търговска среда. Ако използвате Pangolin за лична употреба, можете да игнорирате този раздел.", + "licenseKeyActivated": "Лицензионният ключ е активиран", + "licenseKeyActivatedDescription": "Лицензионният ключ беше успешно активиран.", + "licenseErrorKeyRecheck": "Неуспешно повторно проверяване на лицензионните ключове", + "licenseErrorKeyRecheckDescription": "Възникна грешка при повторно проверяване на лицензионните ключове.", + "licenseErrorKeyRechecked": "Лицензионните ключове бяха повторно проверени", + "licenseErrorKeyRecheckedDescription": "Всички лицензионни ключове бяха повторно проверени", + "licenseActivateKey": "Активиране на лицензионен ключ", + "licenseActivateKeyDescription": "Въведете лицензионен ключ, за да го активирате.", + "licenseActivate": "Активиране на лицензията", + "licenseAgreement": "Чрез поставяне на отметка в това поле потвърждавате, че сте прочели и се съгласявате с лицензионните условия, съответстващи на нивото, свързано с Вашия лицензионен ключ.", + "fossorialLicense": "Преглед на търговски условия и абонамент за Fossorial", + "licenseMessageRemove": "Това ще премахне лицензионния ключ и всички свързани права, предоставени от него.", + "licenseMessageConfirm": "За да потвърдите, въведете лицензионния ключ по-долу.", + "licenseQuestionRemove": "Сигурни ли сте, че искате да изтриете лицензионния ключ {selectedKey}?", + "licenseKeyDelete": "Изтриване на лицензионен ключ", + "licenseKeyDeleteConfirm": "Потвърдете изтриването на лицензионен ключ", + "licenseTitle": "Управление на лицензионния статус", + "licenseTitleDescription": "Преглед и управление на лицензионни ключове в системата", + "licenseHost": "Лиценз за хост", + "licenseHostDescription": "Управление на главния лицензионен ключ за хоста.", + "licensedNot": "Не е лицензиран", + "hostId": "Идентификатор на хост", + "licenseReckeckAll": "Повторно проверяване на всички ключове", + "licenseSiteUsage": "Използване на сайтове", + "licenseSiteUsageDecsription": "Преглед на броя на сайтовете, които използват този лиценз.", + "licenseNoSiteLimit": "Няма лимит за броя на сайтовете, използващи нелицензиран хост.", + "licensePurchase": "Закупуване на лиценз", + "licensePurchaseSites": "Закупуване на допълнителни сайтове", + "licenseSitesUsedMax": "{usedSites} от {maxSites} сайтове използвани", + "licenseSitesUsed": "{count, plural, =0 {# сайта} one {# сайт} other {# сайта}} в системата.", + "licensePurchaseDescription": "Изберете колко сайтове искате да {selectedMode, select, license {закупите лиценз за. Можете винаги да добавите повече сайтове по-късно.} other {добавите към съществуващия си лиценз.}}", + "licenseFee": "Такса за лиценз", + "licensePriceSite": "Цена на сайт", + "total": "Общо", + "licenseContinuePayment": "Продължете към плащане", + "pricingPage": "страница с цени", + "pricingPortal": "Преглед на портала за закупуване", + "licensePricingPage": "За най-актуални цени и отстъпки, моля, посетете ", + "invite": "Покани", + "inviteRegenerate": "Регениране на покана", + "inviteRegenerateDescription": "Отменете предишната покана и създайте нова", + "inviteRemove": "Премахване на покана", + "inviteRemoveError": "Неуспешно премахване на покана", + "inviteRemoveErrorDescription": "Възникна грешка при премахване на поканата.", + "inviteRemoved": "Поканата е премахната", + "inviteRemovedDescription": "Поканата за {имейл} е премахната.", + "inviteQuestionRemove": "Сигурни ли сте, че искате да премахнете поканата {email}?", + "inviteMessageRemove": "След като бъде премахната, тази покана няма да е валидна. Винаги можете да поканите потребителя отново по-късно.", + "inviteMessageConfirm": "За да потвърдите, въведете имейл адреса на поканата по-долу.", + "inviteQuestionRegenerate": "Сигурни ли сте, че искате да регенерирате поканата за {email}? Това ще отмени предишната покана.", + "inviteRemoveConfirm": "Потвърждение на премахването на поканата", + "inviteRegenerated": "Поканата е регенерирана", + "inviteSent": "Нова покана е изпратена на {email}.", + "inviteSentEmail": "Изпращане на имейл известие до потребителя", + "inviteGenerate": "Нова покана е генерирана за {email}.", + "inviteDuplicateError": "Дублиране на покана", + "inviteDuplicateErrorDescription": "Покана за този потребител вече съществува.", + "inviteRateLimitError": "Лимитът на регенерации е надвишен", + "inviteRateLimitErrorDescription": "Надвишили сте лимита от 3 регенерации на час. Моля, опитайте отново по-късно.", + "inviteRegenerateError": "Неуспешно регениране на поканата", + "inviteRegenerateErrorDescription": "Възникна грешка при регенирането на поканата.", + "inviteValidityPeriod": "Период на валидност", + "inviteValidityPeriodSelect": "Изберете период на валидност", + "inviteRegenerateMessage": "Поканата е регенерирана. Потребителят трябва да достъпи линка по-долу, за да приеме поканата.", + "inviteRegenerateButton": "Регениране", + "expiresAt": "Изтича на", + "accessRoleUnknown": "Непозната роля", + "placeholder": "Запълване", + "userErrorOrgRemove": "Неуспешно премахване на потребител", + "userErrorOrgRemoveDescription": "Възникна грешка при премахване на потребителя.", + "userOrgRemoved": "Потребителят е премахнат", + "userOrgRemovedDescription": "Потребителят {email} беше премахнат от организацията.", + "userQuestionOrgRemove": "Сигурни ли сте, че искате да премахнете {email} от организацията?", + "userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.", + "userMessageOrgConfirm": "За да потвърдите, въведете името на потребителя по-долу.", + "userRemoveOrgConfirm": "Потвърдете премахването на потребителя", + "userRemoveOrg": "Премахване на потребителя от организацията", + "users": "Потребители", + "accessRoleMember": "Член", + "accessRoleOwner": "Собственик", + "userConfirmed": "Потвърдено", + "idpNameInternal": "Вътрешен", + "emailInvalid": "Невалиден имейл адрес", + "inviteValidityDuration": "Моля, изберете продължителност", + "accessRoleSelectPlease": "Моля, изберете роля", + "usernameRequired": "Необходимо е потребителско име", + "idpSelectPlease": "Моля, изберете доставчик на идентичност", + "idpGenericOidc": "Основен OAuth2/OIDC доставчик.", + "accessRoleErrorFetch": "Неуспешно извличане на роли", + "accessRoleErrorFetchDescription": "Възникна грешка при извличане на ролите", + "idpErrorFetch": "Неуспешно извличане на доставчици на идентичност", + "idpErrorFetchDescription": "Възникна грешка при извличане на доставчиците на идентичност", + "userErrorExists": "Потребителят вече съществува", + "userErrorExistsDescription": "Този потребител вече е член на организацията.", + "inviteError": "Неуспешно поканване на потребител", + "inviteErrorDescription": "Възникна грешка при поканването на потребителя", + "userInvited": "Потребителят е поканен", + "userInvitedDescription": "Потребителят беше успешно поканен.", + "userErrorCreate": "Неуспешно създаване на потребител", + "userErrorCreateDescription": "Възникна грешка при създаване на потребителя", + "userCreated": "Потребителят е създаден", + "userCreatedDescription": "Потребителят беше успешно създаден.", + "userTypeInternal": "Вътрешен потребител", + "userTypeInternalDescription": "Поканете потребител да се присъедини директно към вашата организация.", + "userTypeExternal": "Външен потребител", + "userTypeExternalDescription": "Създайте потребител с външен доставчик на идентичност.", + "accessUserCreateDescription": "Следвайте стъпките по-долу, за да създадете нов потребител", + "userSeeAll": "Виж всички потребители", + "userTypeTitle": "Тип потребител", + "userTypeDescription": "Определете как искате да създадете потребителя", + "userSettings": "Информация за потребителя", + "userSettingsDescription": "Въведете данните за новия потребител", + "inviteEmailSent": "Изпратете покана по имейл до потребителя", + "inviteValid": "Валидна за", + "selectDuration": "Изберете продължителност", + "accessRoleSelect": "Изберете роля", + "inviteEmailSentDescription": "Имейлът е изпратен до потребителя с достъпния линк по-долу. Те трябва да достъпят линка, за да приемат поканата.", + "inviteSentDescription": "Потребителят е поканен. Те трябва да достъпят линка по-долу, за да приемат поканата.", + "inviteExpiresIn": "Поканата ще изтече след {days, plural, one {# ден} other {# дни}}.", + "idpTitle": "Доставчик на идентичност", + "idpSelect": "Изберете доставчика на идентичност за външния потребител", + "idpNotConfigured": "Няма конфигурирани доставчици на идентичност. Моля, конфигурирайте доставчик на идентичност, преди да създавате външни потребители.", + "usernameUniq": "Това трябва да съответства на уникалното потребителско име, което съществува във избрания доставчик на идентичност.", + "emailOptional": "Имейл (по избор)", + "nameOptional": "Име (по избор)", + "accessControls": "Контрол на достъпа", + "userDescription2": "Управление на настройките на този потребител", + "accessRoleErrorAdd": "Неуспешно добавяне на потребител към роля", + "accessRoleErrorAddDescription": "Възникна грешка при добавяне на потребителя към ролята.", + "userSaved": "Потребителят е запазен", + "userSavedDescription": "Потребителят беше актуализиран.", + "autoProvisioned": "Автоматично предоставено", + "autoProvisionedDescription": "Позволете този потребител да бъде автоматично управляван от доставчик на идентификационни данни", + "accessControlsDescription": "Управлявайте какво може да достъпва и прави този потребител в организацията", + "accessControlsSubmit": "Запазване на контролите за достъп", + "roles": "Роли", + "accessUsersRoles": "Управление на потребители и роли", + "accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до вашата организация", + "key": "Ключ", + "createdAt": "Създаден на", + "proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.", + "proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.", + "proxyEnableSSL": "Активиране на SSL", + "proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.", + "target": "Цел", + "configureTarget": "Конфигуриране на цели", + "targetErrorFetch": "Неуспешно извличане на цели", + "targetErrorFetchDescription": "Възникна грешка при извличане на целите", + "siteErrorFetch": "Неуспешно извличане на ресурс", + "siteErrorFetchDescription": "Възникна грешка при извличане на ресурса", + "targetErrorDuplicate": "Дубликат на цел", + "targetErrorDuplicateDescription": "Цел с тези настройки вече съществува", + "targetWireGuardErrorInvalidIp": "Невалиден таргет IP", + "targetWireGuardErrorInvalidIpDescription": "Таргет IP трябва да бъде в рамките на подмрежата на сайта", + "targetsUpdated": "Целите са актуализирани", + "targetsUpdatedDescription": "Целите и настройките бяха успешно актуализирани", + "targetsErrorUpdate": "Неуспешно актуализиране на целите", + "targetsErrorUpdateDescription": "Възникна грешка при актуализиране на целите", + "targetTlsUpdate": "Настройките на TLS са актуализирани", + "targetTlsUpdateDescription": "Вашите настройки на TLS бяха успешно актуализирани", + "targetErrorTlsUpdate": "Неуспешно актуализиране на настройки на TLS", + "targetErrorTlsUpdateDescription": "Възникна грешка при актуализиране на настройки на TLS", + "proxyUpdated": "Настройките на прокси са актуализирани", + "proxyUpdatedDescription": "Вашите настройки на прокси бяха успешно актуализирани", + "proxyErrorUpdate": "Неуспешно актуализиране на настройки на прокси", + "proxyErrorUpdateDescription": "Възникна грешка при актуализиране на настройки на прокси", + "targetAddr": "IP / Хост име", + "targetPort": "Порт", + "targetProtocol": "Протокол", + "targetTlsSettings": "Конфигурация на защитена връзка", + "targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс", + "targetTlsSettingsAdvanced": "Разширени TLS настройки", + "targetTlsSni": "Имя на TLS сървър", + "targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.", + "targetTlsSubmit": "Запазване на настройките", + "targets": "Конфигурация на целите", + "targetsDescription": "Настройте цели за маршрутиране на трафик към вашите бекенд услуги", + "targetStickySessions": "Активиране на постоянни сесии", + "targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.", + "methodSelect": "Изберете метод", + "targetSubmit": "Добавяне на цел", + "targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.", + "targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.", + "targetsSubmit": "Запазване на целите", + "addTarget": "Добавете цел", + "targetErrorInvalidIp": "Невалиден IP адрес", + "targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост", + "targetErrorInvalidPort": "Невалиден порт", + "targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт", + "targetErrorNoSite": "Няма избран сайт", + "targetErrorNoSiteDescription": "Моля, изберете сайт за целта", + "targetCreated": "Целта е създадена", + "targetCreatedDescription": "Целта беше успешно създадена", + "targetErrorCreate": "Неуспешно създаване на целта", + "targetErrorCreateDescription": "Възникна грешка при създаването на целта", + "save": "Запази", + "proxyAdditional": "Допълнителни настройки на прокси", + "proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки", + "proxyCustomHeader": "Персонализиран хост заглавие", + "proxyCustomHeaderDescription": "Хост заглавието, което да зададете при прокси заявките. Оставете празно, за да използвате подразбиране.", + "proxyAdditionalSubmit": "Запазване на прокси настройките", + "subnetMaskErrorInvalid": "Невалидна маска на мрежа. Трябва да е между 0 и 32.", + "ipAddressErrorInvalidFormat": "Невалиден формат на IP адрес", + "ipAddressErrorInvalidOctet": "Невалиден октет на IP адрес", + "path": "Път", + "matchPath": "Път на съвпадение", + "ipAddressRange": "IP обхват", + "rulesErrorFetch": "Неуспешно извличане на правила", + "rulesErrorFetchDescription": "Възникна грешка при извличане на правилата", + "rulesErrorDuplicate": "Дубликат на правило", + "rulesErrorDuplicateDescription": "Правило с тези настройки вече съществува", + "rulesErrorInvalidIpAddressRange": "Невалиден CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "Моля, въведете валидна стойност на CIDR", + "rulesErrorInvalidUrl": "Невалиден URL път", + "rulesErrorInvalidUrlDescription": "Моля, въведете валидна стойност за URL път", + "rulesErrorInvalidIpAddress": "Невалиден IP", + "rulesErrorInvalidIpAddressDescription": "Моля, въведете валиден IP адрес", + "rulesErrorUpdate": "Неуспешно актуализиране на правилата", + "rulesErrorUpdateDescription": "Възникна грешка при актуализиране на правилата", + "rulesUpdated": "Активиране на правилата", + "rulesUpdatedDescription": "Оценяването на правилата беше актуализирано", + "rulesMatchIpAddressRangeDescription": "Въведете адрес във формат CIDR (напр. 103.21.244.0/22)", + "rulesMatchIpAddress": "Въведете IP адрес (напр. 103.21.244.12)", + "rulesMatchUrl": "Въведете URL път или модел (напр. /api/v1/todos или /api/v1/*)", + "rulesErrorInvalidPriority": "Невалиден приоритет", + "rulesErrorInvalidPriorityDescription": "Моля, въведете валиден приоритет", + "rulesErrorDuplicatePriority": "Дублирани приоритети", + "rulesErrorDuplicatePriorityDescription": "Моля, въведете уникални приоритети", + "ruleUpdated": "Правилата са актуализирани", + "ruleUpdatedDescription": "Правилата бяха успешно актуализирани", + "ruleErrorUpdate": "Операцията не бе успешна", + "ruleErrorUpdateDescription": "Възникна грешка по време на операцията за запис", + "rulesPriority": "Приоритет", + "rulesAction": "Действие", + "rulesMatchType": "Тип на съвпадение", + "value": "Стойност", + "rulesAbout": "Относно правилата", + "rulesAboutDescription": "Правилата ви позволяват да контролирате достъпа до вашия ресурс въз основа на набор от критерии. Можете да създавате правила за разрешаване или отказ на достъп въз основа на IP адрес или URL път.", + "rulesActions": "Действия", + "rulesActionAlwaysAllow": "Винаги позволи: заобикаля всички методи за автентикация", + "rulesActionAlwaysDeny": "Винаги отказвай: блокиране на всички заявки; не може да се направи опит за автентикация", + "rulesActionPassToAuth": "Прехвърляне към удостоверяване: Позволяване опити за методи на удостоверяване", + "rulesMatchCriteria": "Критерии за съответствие", + "rulesMatchCriteriaIpAddress": "Съответствие с конкретен IP адрес", + "rulesMatchCriteriaIpAddressRange": "Съответства на диапазон от IP адреси в CIDR нотация", + "rulesMatchCriteriaUrl": "Съответствие с път или шаблон URL", + "rulesEnable": "Активирай правилата", + "rulesEnableDescription": "Активиране или деактивиране на оценката на правилата за този ресурс", + "rulesResource": "Конфигурация на правилата за ресурси", + "rulesResourceDescription": "Конфигурирайте правила, за да контролирате достъпа до вашия ресурс", + "ruleSubmit": "Добави правило", + "rulesNoOne": "Няма правила. Добавете правило чрез формуляра.", + "rulesOrder": "Правилата се оценяват по приоритет в нарастващ ред.", + "rulesSubmit": "Запазване на правилата", + "resourceErrorCreate": "Грешка при създаване на ресурс", + "resourceErrorCreateDescription": "Възникна грешка при създаването на ресурса", + "resourceErrorCreateMessage": "Грешка при създаване на ресурс:", + "resourceErrorCreateMessageDescription": "Възникна неочаквана грешка", + "sitesErrorFetch": "Грешка при получаване на сайтове", + "sitesErrorFetchDescription": "Възникна грешка при получаването на сайтовете", + "domainsErrorFetch": "Грешка при получаването на домейни", + "domainsErrorFetchDescription": "Възникна грешка при получаването на домейните", + "none": "Няма", + "unknown": "Неизвестно", + "resources": "Ресурси", + "resourcesDescription": "Ресурсите са проксита за приложения, работещи във вашата частна мрежа. Създайте ресурс за всеки HTTP/HTTPS или суров TCP/UDP услуга във вашата частна мрежа. Всеки ресурс трябва да бъде свързан със сайт, за да се осигури частна, сигурна свързаност чрез криптиран WireGuard тунел.", + "resourcesWireGuardConnect": "Сигурно свързване с криптиране на WireGuard", + "resourcesMultipleAuthenticationMethods": "Конфигуриране на множество методи за автентикация", + "resourcesUsersRolesAccess": "Контрол на достъпа, базиран на потребители и роли", + "resourcesErrorUpdate": "Неуспешно превключване на ресурса", + "resourcesErrorUpdateDescription": "Възникна грешка при актуализиране на ресурса", + "access": "Достъп", + "shareLink": "{resource} Сподели връзка", + "resourceSelect": "Изберете ресурс", + "shareLinks": "Споделени връзки", + "share": "Споделени връзки", + "shareDescription2": "Създайте споделени връзки към вашите ресурси. Връзките осигуряват временно или неограничено достъп до вашия ресурс. Можете да конфигурирате продължителността на изтичане на връзката при създаването й.", + "shareEasyCreate": "Лесно за създаване и споделяне", + "shareConfigurableExpirationDuration": "Конфигурируемо време на изтичане", + "shareSecureAndRevocable": "Сигурни и отменяеми", + "nameMin": "Името трябва да съдържа поне {len} знака.", + "nameMax": "Името не трябва да е по-дълго от {len} знака.", + "sitesConfirmCopy": "Моля, потвърдете, че сте копирали конфигурацията.", + "unknownCommand": "Неизвестна команда", + "newtErrorFetchReleases": "Неуспешно получаване на информация за изданието: {err}", + "newtErrorFetchLatest": "Грешка при получаването на последното издание: {err}", + "newtEndpoint": "Newt Изходен пункт", "newtId": "Newt ID", "newtSecretKey": "Newt Secret Key", - "architecture": "Architecture", - "sites": "Sites", - "siteWgAnyClients": "Use any WireGuard client to connect. You will have to address your internal resources using the peer IP.", - "siteWgCompatibleAllClients": "Compatible with all WireGuard clients", - "siteWgManualConfigurationRequired": "Manual configuration required", - "userErrorNotAdminOrOwner": "User is not an admin or owner", - "pangolinSettings": "Settings - Pangolin", - "accessRoleYour": "Your role:", - "accessRoleSelect2": "Select a role", - "accessUserSelect": "Select a user", - "otpEmailEnter": "Enter an email", - "otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.", - "otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.", - "otpEmailSmtpRequired": "SMTP Required", - "otpEmailSmtpRequiredDescription": "SMTP must be enabled on the server to use one-time password authentication.", - "otpEmailTitle": "One-time Passwords", - "otpEmailTitleDescription": "Require email-based authentication for resource access", - "otpEmailWhitelist": "Email Whitelist", - "otpEmailWhitelistList": "Whitelisted Emails", - "otpEmailWhitelistListDescription": "Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain.", - "otpEmailWhitelistSave": "Save Whitelist", - "passwordAdd": "Add Password", - "passwordRemove": "Remove Password", - "pincodeAdd": "Add PIN Code", - "pincodeRemove": "Remove PIN Code", - "resourceAuthMethods": "Authentication Methods", - "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", - "resourceAuthSettingsSave": "Saved successfully", - "resourceAuthSettingsSaveDescription": "Authentication settings have been saved", - "resourceErrorAuthFetch": "Failed to fetch data", - "resourceErrorAuthFetchDescription": "An error occurred while fetching the data", - "resourceErrorPasswordRemove": "Error removing resource password", - "resourceErrorPasswordRemoveDescription": "An error occurred while removing the resource password", - "resourceErrorPasswordSetup": "Error setting resource password", - "resourceErrorPasswordSetupDescription": "An error occurred while setting the resource password", - "resourceErrorPincodeRemove": "Error removing resource pincode", - "resourceErrorPincodeRemoveDescription": "An error occurred while removing the resource pincode", - "resourceErrorPincodeSetup": "Error setting resource PIN code", - "resourceErrorPincodeSetupDescription": "An error occurred while setting the resource PIN code", - "resourceErrorUsersRolesSave": "Failed to set roles", - "resourceErrorUsersRolesSaveDescription": "An error occurred while setting the roles", - "resourceErrorWhitelistSave": "Failed to save whitelist", - "resourceErrorWhitelistSaveDescription": "An error occurred while saving the whitelist", - "resourcePasswordSubmit": "Enable Password Protection", - "resourcePasswordProtection": "Password Protection {status}", - "resourcePasswordRemove": "Resource password removed", - "resourcePasswordRemoveDescription": "The resource password has been removed successfully", - "resourcePasswordSetup": "Resource password set", - "resourcePasswordSetupDescription": "The resource password has been set successfully", - "resourcePasswordSetupTitle": "Set Password", - "resourcePasswordSetupTitleDescription": "Set a password to protect this resource", - "resourcePincode": "PIN Code", - "resourcePincodeSubmit": "Enable PIN Code Protection", - "resourcePincodeProtection": "PIN Code Protection {status}", - "resourcePincodeRemove": "Resource pincode removed", - "resourcePincodeRemoveDescription": "The resource password has been removed successfully", - "resourcePincodeSetup": "Resource PIN code set", - "resourcePincodeSetupDescription": "The resource pincode has been set successfully", - "resourcePincodeSetupTitle": "Set Pincode", - "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", - "resourceRoleDescription": "Admins can always access this resource.", - "resourceUsersRoles": "Users & Roles", - "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", - "resourceUsersRolesSubmit": "Save Users & Roles", - "resourceWhitelistSave": "Saved successfully", - "resourceWhitelistSaveDescription": "Whitelist settings have been saved", - "ssoUse": "Use Platform SSO", - "ssoUseDescription": "Existing users will only have to log in once for all resources that have this enabled.", - "proxyErrorInvalidPort": "Invalid port number", - "subdomainErrorInvalid": "Invalid subdomain", - "domainErrorFetch": "Error fetching domains", - "domainErrorFetchDescription": "An error occurred when fetching the domains", - "resourceErrorUpdate": "Failed to update resource", - "resourceErrorUpdateDescription": "An error occurred while updating the resource", - "resourceUpdated": "Resource updated", - "resourceUpdatedDescription": "The resource has been updated successfully", - "resourceErrorTransfer": "Failed to transfer resource", - "resourceErrorTransferDescription": "An error occurred while transferring the resource", - "resourceTransferred": "Resource transferred", - "resourceTransferredDescription": "The resource has been transferred successfully", - "resourceErrorToggle": "Failed to toggle resource", - "resourceErrorToggleDescription": "An error occurred while updating the resource", - "resourceVisibilityTitle": "Visibility", - "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", - "resourceGeneral": "General Settings", - "resourceGeneralDescription": "Configure the general settings for this resource", - "resourceEnable": "Enable Resource", - "resourceTransfer": "Transfer Resource", - "resourceTransferDescription": "Transfer this resource to a different site", - "resourceTransferSubmit": "Transfer Resource", - "siteDestination": "Destination Site", - "searchSites": "Search sites", - "accessRoleCreate": "Create Role", - "accessRoleCreateDescription": "Create a new role to group users and manage their permissions.", - "accessRoleCreateSubmit": "Create Role", - "accessRoleCreated": "Role created", - "accessRoleCreatedDescription": "The role has been successfully created.", - "accessRoleErrorCreate": "Failed to create role", - "accessRoleErrorCreateDescription": "An error occurred while creating the role.", - "accessRoleErrorNewRequired": "New role is required", - "accessRoleErrorRemove": "Failed to remove role", - "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", - "accessRoleName": "Role Name", - "accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.", - "accessRoleRemove": "Remove Role", - "accessRoleRemoveDescription": "Remove a role from the organization", - "accessRoleRemoveSubmit": "Remove Role", - "accessRoleRemoved": "Role removed", - "accessRoleRemovedDescription": "The role has been successfully removed.", - "accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.", - "manage": "Manage", - "sitesNotFound": "No sites found.", - "pangolinServerAdmin": "Server Admin - Pangolin", - "licenseTierProfessional": "Professional License", - "licenseTierEnterprise": "Enterprise License", - "licenseTierCommercial": "Commercial License", - "licensed": "Licensed", - "yes": "Yes", - "no": "No", - "sitesAdditional": "Additional Sites", - "licenseKeys": "License Keys", - "sitestCountDecrease": "Decrease site count", - "sitestCountIncrease": "Increase site count", - "idpManage": "Manage Identity Providers", - "idpManageDescription": "View and manage identity providers in the system", - "idpDeletedDescription": "Identity provider deleted successfully", + "architecture": "Архитектура", + "sites": "Сайтове", + "siteWgAnyClients": "Използвайте всеки WireGuard клиент за свързване. Ще трябва да адресирате вашите вътрешни ресурси, използвайки IP на равностойния.", + "siteWgCompatibleAllClients": "Съвместим с всички WireGuard клиенти", + "siteWgManualConfigurationRequired": "Необходима е ръчна конфигурация", + "userErrorNotAdminOrOwner": "Потребителят не е администратор или собственик", + "pangolinSettings": "Настройки - Панголин", + "accessRoleYour": "Вашата роля:", + "accessRoleSelect2": "Изберете роля", + "accessUserSelect": "Изберете потребител", + "otpEmailEnter": "Въведете имейл", + "otpEmailEnterDescription": "Натиснете Enter, за да добавите имейл след като сте го въведели в полето за въвеждане.", + "otpEmailErrorInvalid": "Невалиден имейл адрес. Wilcard (*) трябва да е цялата част от локалния адрес.", + "otpEmailSmtpRequired": "Необходимо е SMTP", + "otpEmailSmtpRequiredDescription": "SMTP трябва да бъде активиран на сървъра, за да използвате еднократни пароли за автентикация.", + "otpEmailTitle": "Еднократни пароли", + "otpEmailTitleDescription": "Изисквайте автентикация базирана на имейл за достъп до ресурси", + "otpEmailWhitelist": "Бял списък на имейли", + "otpEmailWhitelistList": "Имейли в белия списък", + "otpEmailWhitelistListDescription": "Само потребители с тези имейл адреси ще могат да имат достъп до този ресурс. Те ще бъдат помолени да въведат еднократна парола, изпратена на техния имейл. Може да се използват wildcards (*@example.com), за да се позволи на всеки имейл адрес от домейн.", + "otpEmailWhitelistSave": "Запазване на белия списък", + "passwordAdd": "Добави парола", + "passwordRemove": "Премахни парола", + "pincodeAdd": "Добави ПИН код", + "pincodeRemove": "Премахни ПИН код", + "resourceAuthMethods": "Методи за автентикация", + "resourceAuthMethodsDescriptions": "Позволете достъп до ресурса чрез допълнителни методи за автентикация", + "resourceAuthSettingsSave": "Запазено успешно", + "resourceAuthSettingsSaveDescription": "Настройките за автентикация са запазени успешно", + "resourceErrorAuthFetch": "Неуспешно получаване на данни", + "resourceErrorAuthFetchDescription": "Възникна грешка при получаването на данните", + "resourceErrorPasswordRemove": "Грешка при премахване на паролата за ресурс", + "resourceErrorPasswordRemoveDescription": "Възникна грешка при премахването на паролата за ресурс", + "resourceErrorPasswordSetup": "Грешка при настройване на паролата за ресурс", + "resourceErrorPasswordSetupDescription": "Възникна грешка при настройването на паролата за ресурс", + "resourceErrorPincodeRemove": "Грешка при премахване на ПИН кода за ресурс", + "resourceErrorPincodeRemoveDescription": "Възникна грешка при премахването на ПИН кода за ресурс", + "resourceErrorPincodeSetup": "Грешка при настройване на ПИН кода за ресурс", + "resourceErrorPincodeSetupDescription": "Възникна грешка при настройването на ПИН кода за ресурс", + "resourceErrorUsersRolesSave": "Неуспешно задаване на роли", + "resourceErrorUsersRolesSaveDescription": "Възникна грешка при задаването на ролите", + "resourceErrorWhitelistSave": "Неуспешно запазване на белия списък", + "resourceErrorWhitelistSaveDescription": "Възникна грешка при запазването на белия списък", + "resourcePasswordSubmit": "Активирай защита с парола", + "resourcePasswordProtection": "Защита с парола {status}", + "resourcePasswordRemove": "Паролата на ресурса е премахната", + "resourcePasswordRemoveDescription": "Премахването на паролата за ресурс беше успешно", + "resourcePasswordSetup": "Паролата за ресурс е настроена", + "resourcePasswordSetupDescription": "Паролата за ресурс беше успешно настроена", + "resourcePasswordSetupTitle": "Задай парола", + "resourcePasswordSetupTitleDescription": "Задайте парола, за да защитите този ресурс", + "resourcePincode": "ПИН код", + "resourcePincodeSubmit": "Активирай защита с ПИН код", + "resourcePincodeProtection": "Защита с ПИН код {status}", + "resourcePincodeRemove": "Премахнат ПИН код за ресурс", + "resourcePincodeRemoveDescription": "Премахването на ПИН кода за ресурс беше успешно", + "resourcePincodeSetup": "Настроен ПИН код за ресурс", + "resourcePincodeSetupDescription": "Настройването на ПИН кода за ресурс беше успешно", + "resourcePincodeSetupTitle": "Задай ПИН код", + "resourcePincodeSetupTitleDescription": "Задайте ПИН код, за да защитите този ресурс", + "resourceRoleDescription": "Администраторите винаги могат да имат достъп до този ресурс.", + "resourceUsersRoles": "Потребители и роли", + "resourceUsersRolesDescription": "Конфигурирайте кои потребители и роли могат да посещават този ресурс", + "resourceUsersRolesSubmit": "Запазете потребители и роли", + "resourceWhitelistSave": "Успешно запазено", + "resourceWhitelistSaveDescription": "Настройките на белия списък са запазени", + "ssoUse": "Използвай платформен SSO", + "ssoUseDescription": "Съществуващите потребители ще трябва да влязат само веднъж за всички ресурси, които имат тази опция активирана.", + "proxyErrorInvalidPort": "Невалиден номер на порт", + "subdomainErrorInvalid": "Невалиден поддомейн", + "domainErrorFetch": "Грешка при получаването на домейни", + "domainErrorFetchDescription": "Възникна грешка при получаването на домейните", + "resourceErrorUpdate": "Неуспешно актуализиране на ресурса", + "resourceErrorUpdateDescription": "Възникна грешка при актуализирането на ресурса", + "resourceUpdated": "Ресурсът е обновен", + "resourceUpdatedDescription": "Ресурсът беше успешно обновен", + "resourceErrorTransfer": "Неуспешно прехвърляне на ресурса", + "resourceErrorTransferDescription": "Възникна грешка при прехвърлянето на ресурса", + "resourceTransferred": "Ресурсът е прехвърлен", + "resourceTransferredDescription": "Ресурсът беше успешно прехвърлен", + "resourceErrorToggle": "Неуспешно превключване на ресурса", + "resourceErrorToggleDescription": "Възникна грешка при актуализирането на ресурса", + "resourceVisibilityTitle": "Видимост", + "resourceVisibilityTitleDescription": "Напълно активирайте или деактивирайте видимостта на ресурса", + "resourceGeneral": "Общи настройки", + "resourceGeneralDescription": "Конфигурирайте общите настройки за този ресурс", + "resourceEnable": "Активирайте ресурс", + "resourceTransfer": "Прехвърлете ресурс", + "resourceTransferDescription": "Прехвърлете този ресурс към различен сайт", + "resourceTransferSubmit": "Прехвърлете ресурс", + "siteDestination": "Дестинационен сайт", + "searchSites": "Търси сайтове", + "accessRoleCreate": "Създайте роля", + "accessRoleCreateDescription": "Създайте нова роля за групиране на потребители и управление на техните разрешения.", + "accessRoleCreateSubmit": "Създайте роля", + "accessRoleCreated": "Ролята е създадена", + "accessRoleCreatedDescription": "Ролята беше успешно създадена.", + "accessRoleErrorCreate": "Неуспешно създаване на роля", + "accessRoleErrorCreateDescription": "Възникна грешка при създаването на ролята.", + "accessRoleErrorNewRequired": "Нова роля е необходима", + "accessRoleErrorRemove": "Неуспешно премахване на роля", + "accessRoleErrorRemoveDescription": "Възникна грешка при премахването на роля.", + "accessRoleName": "Име на роля", + "accessRoleQuestionRemove": "Ще изтриете ролята {name}. Не можете да отмените това действие.", + "accessRoleRemove": "Премахни роля", + "accessRoleRemoveDescription": "Премахни роля от организацията", + "accessRoleRemoveSubmit": "Премахни роля", + "accessRoleRemoved": "Ролята е премахната", + "accessRoleRemovedDescription": "Ролята беше успешно премахната.", + "accessRoleRequiredRemove": "Преди да изтриете тази роля, моля изберете нова роля, към която да прехвърлите настоящите членове.", + "manage": "Управление", + "sitesNotFound": "Няма намерени сайтове.", + "pangolinServerAdmin": "Администратор на сървър - Панголин", + "licenseTierProfessional": "Професионален лиценз", + "licenseTierEnterprise": "Предприятие лиценз", + "licenseTierPersonal": "Personal License", + "licensed": "Лицензиран", + "yes": "Да", + "no": "Не", + "sitesAdditional": "Допълнителни сайтове", + "licenseKeys": "Лицензионни ключове", + "sitestCountDecrease": "Намаляване на броя на сайтовете", + "sitestCountIncrease": "Увеличаване на броя на сайтовете", + "idpManage": "Управление на доставчици на идентичност", + "idpManageDescription": "Прегледайте и управлявайте доставчици на идентичност в системата", + "idpDeletedDescription": "Доставчик на идентичност успешно изтрит", "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?", - "idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.", - "idpMessageConfirm": "To confirm, please type the name of the identity provider below.", - "idpConfirmDelete": "Confirm Delete Identity Provider", - "idpDelete": "Delete Identity Provider", - "idp": "Identity Providers", - "idpSearch": "Search identity providers...", - "idpAdd": "Add Identity Provider", - "idpClientIdRequired": "Client ID is required.", - "idpClientSecretRequired": "Client Secret is required.", - "idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.", - "idpErrorTokenUrlInvalid": "Token URL must be a valid URL.", - "idpPathRequired": "Identifier Path is required.", - "idpScopeRequired": "Scopes are required.", - "idpOidcDescription": "Configure an OpenID Connect identity provider", - "idpCreatedDescription": "Identity provider created successfully", - "idpCreate": "Create Identity Provider", - "idpCreateDescription": "Configure a new identity provider for user authentication", - "idpSeeAll": "See All Identity Providers", - "idpSettingsDescription": "Configure the basic information for your identity provider", - "idpDisplayName": "A display name for this identity provider", - "idpAutoProvisionUsers": "Auto Provision Users", - "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", - "licenseBadge": "Professional", - "idpType": "Provider Type", - "idpTypeDescription": "Select the type of identity provider you want to configure", - "idpOidcConfigure": "OAuth2/OIDC Configuration", - "idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials", - "idpClientId": "Client ID", - "idpClientIdDescription": "The OAuth2 client ID from your identity provider", - "idpClientSecret": "Client Secret", - "idpClientSecretDescription": "The OAuth2 client secret from your identity provider", - "idpAuthUrl": "Authorization URL", - "idpAuthUrlDescription": "The OAuth2 authorization endpoint URL", - "idpTokenUrl": "Token URL", - "idpTokenUrlDescription": "The OAuth2 token endpoint URL", - "idpOidcConfigureAlert": "Important Information", - "idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in your identity provider's settings. The callback URL will be provided after successful creation.", - "idpToken": "Token Configuration", - "idpTokenDescription": "Configure how to extract user information from the ID token", - "idpJmespathAbout": "About JMESPath", - "idpJmespathAboutDescription": "The paths below use JMESPath syntax to extract values from the ID token.", - "idpJmespathAboutDescriptionLink": "Learn more about JMESPath", - "idpJmespathLabel": "Identifier Path", - "idpJmespathLabelDescription": "The path to the user identifier in the ID token", - "idpJmespathEmailPathOptional": "Email Path (Optional)", - "idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token", - "idpJmespathNamePathOptional": "Name Path (Optional)", - "idpJmespathNamePathOptionalDescription": "The path to the user's name in the ID token", - "idpOidcConfigureScopes": "Scopes", - "idpOidcConfigureScopesDescription": "Space-separated list of OAuth2 scopes to request", - "idpSubmit": "Create Identity Provider", - "orgPolicies": "Organization Policies", - "idpSettings": "{idpName} Settings", - "idpCreateSettingsDescription": "Configure the settings for your identity provider", - "roleMapping": "Role Mapping", - "orgMapping": "Organization Mapping", - "orgPoliciesSearch": "Search organization policies...", - "orgPoliciesAdd": "Add Organization Policy", - "orgRequired": "Organization is required", - "error": "Error", - "success": "Success", - "orgPolicyAddedDescription": "Policy added successfully", - "orgPolicyUpdatedDescription": "Policy updated successfully", - "orgPolicyDeletedDescription": "Policy deleted successfully", - "defaultMappingsUpdatedDescription": "Default mappings updated successfully", - "orgPoliciesAbout": "About Organization Policies", - "orgPoliciesAboutDescription": "Organization policies are used to control access to organizations based on the user's ID token. You can specify JMESPath expressions to extract role and organization information from the ID token.", - "orgPoliciesAboutDescriptionLink": "See documentation, for more information.", - "defaultMappingsOptional": "Default Mappings (Optional)", - "defaultMappingsOptionalDescription": "The default mappings are used when when there is not an organization policy defined for an organization. You can specify the default role and organization mappings to fall back to here.", - "defaultMappingsRole": "Default Role Mapping", - "defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.", - "defaultMappingsOrg": "Default Organization Mapping", - "defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.", - "defaultMappingsSubmit": "Save Default Mappings", - "orgPoliciesEdit": "Edit Organization Policy", - "org": "Organization", - "orgSelect": "Select organization", - "orgSearch": "Search org", - "orgNotFound": "No org found.", - "roleMappingPathOptional": "Role Mapping Path (Optional)", - "orgMappingPathOptional": "Organization Mapping Path (Optional)", - "orgPolicyUpdate": "Update Policy", - "orgPolicyAdd": "Add Policy", - "orgPolicyConfig": "Configure access for an organization", - "idpUpdatedDescription": "Identity provider updated successfully", - "redirectUrl": "Redirect URL", - "redirectUrlAbout": "About Redirect URL", - "redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in your identity provider settings.", - "pangolinAuth": "Auth - Pangolin", - "verificationCodeLengthRequirements": "Your verification code must be 8 characters.", - "errorOccurred": "An error occurred", - "emailErrorVerify": "Failed to verify email:", - "emailVerified": "Email successfully verified! Redirecting you...", - "verificationCodeErrorResend": "Failed to resend verification code:", - "verificationCodeResend": "Verification code resent", - "verificationCodeResendDescription": "We've resent a verification code to your email address. Please check your inbox.", - "emailVerify": "Verify Email", - "emailVerifyDescription": "Enter the verification code sent to your email address.", - "verificationCode": "Verification Code", - "verificationCodeEmailSent": "We sent a verification code to your email address.", - "submit": "Submit", - "emailVerifyResendProgress": "Resending...", - "emailVerifyResend": "Didn't receive a code? Click here to resend", - "passwordNotMatch": "Passwords do not match", - "signupError": "An error occurred while signing up", - "pangolinLogoAlt": "Pangolin Logo", - "inviteAlready": "Looks like you've been invited!", - "inviteAlreadyDescription": "To accept the invite, you must log in or create an account.", - "signupQuestion": "Already have an account?", - "login": "Log in", - "resourceNotFound": "Resource Not Found", - "resourceNotFoundDescription": "The resource you're trying to access does not exist.", - "pincodeRequirementsLength": "PIN must be exactly 6 digits", - "pincodeRequirementsChars": "PIN must only contain numbers", - "passwordRequirementsLength": "Password must be at least 1 character long", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", - "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", - "otpEmailRequirementsLength": "OTP must be at least 1 character long", - "otpEmailSent": "OTP Sent", - "otpEmailSentDescription": "An OTP has been sent to your email", - "otpEmailErrorAuthenticate": "Failed to authenticate with email", - "pincodeErrorAuthenticate": "Failed to authenticate with pincode", - "passwordErrorAuthenticate": "Failed to authenticate with password", - "poweredBy": "Powered by", - "authenticationRequired": "Authentication Required", - "authenticationMethodChoose": "Choose your preferred method to access {name}", - "authenticationRequest": "You must authenticate to access {name}", - "user": "User", - "pincodeInput": "6-digit PIN Code", - "pincodeSubmit": "Log in with PIN", - "passwordSubmit": "Log In with Password", - "otpEmailDescription": "A one-time code will be sent to this email.", - "otpEmailSend": "Send One-time Code", - "otpEmail": "One-Time Password (OTP)", - "otpEmailSubmit": "Submit OTP", - "backToEmail": "Back to Email", - "noSupportKey": "Server is running without a supporter key. Consider supporting the project!", - "accessDenied": "Access Denied", - "accessDeniedDescription": "You're not allowed to access this resource. If this is a mistake, please contact the administrator.", - "accessTokenError": "Error checking access token", - "accessGranted": "Access Granted", - "accessUrlInvalid": "Access URL Invalid", - "accessGrantedDescription": "You have been granted access to this resource. Redirecting you...", - "accessUrlInvalidDescription": "This shared access URL is invalid. Please contact the resource owner for a new URL.", - "tokenInvalid": "Invalid token", - "pincodeInvalid": "Invalid code", - "passwordErrorRequestReset": "Failed to request reset:", - "passwordErrorReset": "Failed to reset password:", - "passwordResetSuccess": "Password reset successfully! Back to log in...", - "passwordReset": "Reset Password", - "passwordResetDescription": "Follow the steps to reset your password", - "passwordResetSent": "We'll send a password reset code to this email address.", - "passwordResetCode": "Reset Code", - "passwordResetCodeDescription": "Check your email for the reset code.", - "passwordNew": "New Password", - "passwordNewConfirm": "Confirm New Password", - "pincodeAuth": "Authenticator Code", - "pincodeSubmit2": "Submit Code", - "passwordResetSubmit": "Request Reset", - "passwordBack": "Back to Password", - "loginBack": "Go back to log in", - "signup": "Sign up", - "loginStart": "Log in to get started", - "idpOidcTokenValidating": "Validating OIDC token", - "idpOidcTokenResponse": "Validate OIDC token response", - "idpErrorOidcTokenValidating": "Error validating OIDC token", - "idpConnectingTo": "Connecting to {name}", - "idpConnectingToDescription": "Validating your identity", - "idpConnectingToProcess": "Connecting...", - "idpConnectingToFinished": "Connected", - "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", - "idpErrorNotFound": "IdP not found", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", - "inviteInvalid": "Invalid Invite", - "inviteInvalidDescription": "The invite link is invalid.", - "inviteErrorWrongUser": "Invite is not for this user", - "inviteErrorUserNotExists": "User does not exist. Please create an account first.", - "inviteErrorLoginRequired": "You must be logged in to accept an invite", - "inviteErrorExpired": "The invite may have expired", - "inviteErrorRevoked": "The invite might have been revoked", - "inviteErrorTypo": "There could be a typo in the invite link", - "pangolinSetup": "Setup - Pangolin", - "orgNameRequired": "Organization name is required", - "orgIdRequired": "Organization ID is required", - "orgErrorCreate": "An error occurred while creating org", - "pageNotFound": "Page Not Found", - "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", - "overview": "Overview", - "home": "Home", - "accessControl": "Access Control", - "settings": "Settings", - "usersAll": "All Users", - "license": "License", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "No results found.", + "idpQuestionRemove": "Сигурни ли сте, че искате да изтриете трайно доставчика на идентичност {name}", + "idpMessageRemove": "Това ще премахне доставчика на идентичност и всички свързани конфигурации. Потребителите, които се удостоверяват през този доставчик, вече няма да могат да влязат.", + "idpMessageConfirm": "За потвърждение, моля въведете името на доставчика на идентичност по-долу.", + "idpConfirmDelete": "Потвърдите изтриването на доставчик на идентичност", + "idpDelete": "Изтрий доставчик на идентичност", + "idp": "Доставчици на идентичност", + "idpSearch": "Търси доставчици на идентичност...", + "idpAdd": "Добавяне на доставчик на идентичност", + "idpClientIdRequired": "Необходим е идентификационен номер на клиента.", + "idpClientSecretRequired": "Необходим секретен код на клиента.", + "idpErrorAuthUrlInvalid": "URL за удостоверяване трябва да бъде валиден URL.", + "idpErrorTokenUrlInvalid": "URL токен трябва да бъде валиден URL.", + "idpPathRequired": "Идентификаторът на пътя е необходим.", + "idpScopeRequired": "Областите са задължителни.", + "idpOidcDescription": "Конфигурирайте доставчик на идентичност с OpenID Connect", + "idpCreatedDescription": "Доставчикът на идентичност създаден успешно", + "idpCreate": "Създаване на доставчик на идентичност", + "idpCreateDescription": "Конфигурирайте нов доставчик на идентичност за удостоверяване на потребителя", + "idpSeeAll": "Виж всички доставчици на идентичност", + "idpSettingsDescription": "Конфигурирайте основната информация за вашия доставчик на идентичност", + "idpDisplayName": "Име за показване за този доставчик на идентичност", + "idpAutoProvisionUsers": "Автоматично потребителско създаване", + "idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.", + "licenseBadge": "EE", + "idpType": "Тип доставчик", + "idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате", + "idpOidcConfigure": "Конфигурация на OAuth2/OIDC", + "idpOidcConfigureDescription": "Конфигурирайте OAuth2/OIDC доставчика на крайни точки и кредити", + "idpClientId": "ID на клиента", + "idpClientIdDescription": "OAuth2 идентификационен номер на клиента от вашия доставчик на идентичност", + "idpClientSecret": "Секретен код на клиента", + "idpClientSecretDescription": "OAuth2 секретен код на клиента от вашия доставчик на идентичност", + "idpAuthUrl": "URL за удостоверение", + "idpAuthUrlDescription": "OAuth2 крайна точка за удостоверяване URL", + "idpTokenUrl": "URL на токена", + "idpTokenUrlDescription": "OAuth2 крайна точка на токена URL", + "idpOidcConfigureAlert": "Важно информация", + "idpOidcConfigureAlertDescription": "След създаването на доставчика на идентичност ще е необходимо да конфигурирате URL за обратна връзка в настройките на вашия доставчик на идентичност. URL за обратна връзка ще бъде предоставен след успешно създаване.", + "idpToken": "Конфигуриране на токените", + "idpTokenDescription": "Конфигурирайте как да извлечете информация за потребителя от ID токена", + "idpJmespathAbout": "Относно JMESPath", + "idpJmespathAboutDescription": "Пътищата по-долу използват синтаксиса JMESPath за извличане на стойности от ID токена.", + "idpJmespathAboutDescriptionLink": "Научете повече за JMESPath", + "idpJmespathLabel": "Идентификатор на пътя", + "idpJmespathLabelDescription": "Пътят към идентификатора на потребителя в ID токена", + "idpJmespathEmailPathOptional": "Път за имейл (по избор)", + "idpJmespathEmailPathOptionalDescription": "Пътят до имейла на потребителя в ID токена", + "idpJmespathNamePathOptional": "Път (по избор) на име", + "idpJmespathNamePathOptionalDescription": "Пътят до името на потребителя в ID токена", + "idpOidcConfigureScopes": "Области", + "idpOidcConfigureScopesDescription": "Списък от разделените се с интервали OAuth2 области, които да се заявят", + "idpSubmit": "Създайте доставчик на идентичност", + "orgPolicies": "Организационни политики", + "idpSettings": "{idpName} Настройки", + "idpCreateSettingsDescription": "Конфигурирайте настройките за вашия доставчик на идентичност", + "roleMapping": "Ролева карта", + "orgMapping": "Организационна карта", + "orgPoliciesSearch": "Търсене на организационни политики...", + "orgPoliciesAdd": "Добавяне на организационна политика", + "orgRequired": "Организацията е задължителна", + "error": "Грешка", + "success": "Успех", + "orgPolicyAddedDescription": "Политиката беше добавена успешно", + "orgPolicyUpdatedDescription": "Политиката беше актуализирана успешно", + "orgPolicyDeletedDescription": "Политиката беше изтрита успешно", + "defaultMappingsUpdatedDescription": "Файловете по подразбиране бяха актуализирани успешно", + "orgPoliciesAbout": "За Организационни политики", + "orgPoliciesAboutDescription": "Организационните политики се използват за контрол на достъпа до организации въз основа на идентификационния токен на потребителя. Можете да зададете JMESPath изрази за извличане на роля и информация за организацията от идентификационния токен.", + "orgPoliciesAboutDescriptionLink": "Вижте документацията за повече информация.", + "defaultMappingsOptional": "Файлове по подразбиране (По желание)", + "defaultMappingsOptionalDescription": "Файловете по подразбиране се използват, когато няма дефинирана организационна политика за организацията. Можете да зададете роля и файлове за организацията по подразбиране, които да се използват в този случай.", + "defaultMappingsRole": "Карта на роля по подразбиране", + "defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.", + "defaultMappingsOrg": "Карта на организация по подразбиране", + "defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.", + "defaultMappingsSubmit": "Запазване на файловете по подразбиране", + "orgPoliciesEdit": "Редактиране на Организационна Политика", + "org": "Организация", + "orgSelect": "Изберете организация", + "orgSearch": "Търсене на организация", + "orgNotFound": "Не е намерена организация.", + "roleMappingPathOptional": "Път на ролята (По желание)", + "orgMappingPathOptional": "Път на организацията (По желание)", + "orgPolicyUpdate": "Актуализиране на Политика", + "orgPolicyAdd": "Добавяне на Политика", + "orgPolicyConfig": "Конфигуриране на достъп за организация", + "idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно", + "redirectUrl": "URL за пренасочване", + "redirectUrlAbout": "За URL за пренасочване", + "redirectUrlAboutDescription": "Това е URL, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL в настройките на вашия доставчик на идентификация.", + "pangolinAuth": "Authent - Pangolin", + "verificationCodeLengthRequirements": "Вашият код за удостоверяване трябва да бъде 8 символа.", + "errorOccurred": "Възникна грешка", + "emailErrorVerify": "Неуспешно удостоверяване на имейл:", + "emailVerified": "Имейлът беше успешно удостоверен! Пренасочване...", + "verificationCodeErrorResend": "Неуспешно изпращане на код за удостоверяване отново:", + "verificationCodeResend": "Кодът за удостоверяване беше изпратен отново", + "verificationCodeResendDescription": "Изпратихме код за удостоверяване на вашия имейл адрес. Моля, проверете входящата си поща.", + "emailVerify": "Потвърждаване на имейл", + "emailVerifyDescription": "Въведете кода за удостоверяване, изпратен на вашия имейл адрес.", + "verificationCode": "Код за удостоверяване", + "verificationCodeEmailSent": "Изпратихме код за удостоверяване на вашия имейл адрес.", + "submit": "Изпращане", + "emailVerifyResendProgress": "Пренасочване...", + "emailVerifyResend": "Не получихте код? Кликнете тук, за да изпратите отново", + "passwordNotMatch": "Паролите не съвпадат", + "signupError": "Възникна грешка при регистрация", + "pangolinLogoAlt": "Лого на Pangolin", + "inviteAlready": "Изглежда, че сте били поканени!", + "inviteAlreadyDescription": "За да приемете поканата, трябва да влезете или да създадете акаунт.", + "signupQuestion": "Вече имате акаунт?", + "login": "Влизане", + "resourceNotFound": "Ресурсът не е намерен", + "resourceNotFoundDescription": "Ресурсът, който се опитвате да достъпите, не съществува.", + "pincodeRequirementsLength": "ПИН трябва да бъде точно 6 цифри", + "pincodeRequirementsChars": "ПИН трябва да съдържа само цифри", + "passwordRequirementsLength": "Паролата трябва да бъде поне 1 символа дълга", + "passwordRequirementsTitle": "Изисквания към паролата:", + "passwordRequirementLength": "Поне 8 символа дължина", + "passwordRequirementUppercase": "Поне една главна буква", + "passwordRequirementLowercase": "Поне една малка буква", + "passwordRequirementNumber": "Поне една цифра", + "passwordRequirementSpecial": "Поне един специален символ", + "passwordRequirementsMet": "✓ Паролата отговаря на всички изисквания", + "passwordStrength": "Сила на паролата", + "passwordStrengthWeak": "Слаба", + "passwordStrengthMedium": "Средна", + "passwordStrengthStrong": "Силна", + "passwordRequirements": "Изисквания:", + "passwordRequirementLengthText": "8+ символа", + "passwordRequirementUppercaseText": "Главна буква (A-Z)", + "passwordRequirementLowercaseText": "Малка буква (a-z)", + "passwordRequirementNumberText": "Цифра (0-9)", + "passwordRequirementSpecialText": "Специален символ (!@#$%...)", + "passwordsDoNotMatch": "Паролите не съвпадат", + "otpEmailRequirementsLength": "OTP трябва да бъде поне 1 символа дълга", + "otpEmailSent": "OTP изпратен", + "otpEmailSentDescription": "OTP беше изпратен на вашия имейл", + "otpEmailErrorAuthenticate": "Неуспешно удостоверяване чрез имейл", + "pincodeErrorAuthenticate": "Неуспешно удостоверяване чрез ПИН", + "passwordErrorAuthenticate": "Неуспешно удостоверяване чрез парола", + "poweredBy": "Поддържано от", + "authenticationRequired": "Необходимо е удостоверяване", + "authenticationMethodChoose": "Изберете предпочитаният метод за достъп до {name}", + "authenticationRequest": "Трябва да удостоверите за достъп до {name}", + "user": "Потребител", + "pincodeInput": "6-цифрен ПИН код", + "pincodeSubmit": "Влез с ПИН", + "passwordSubmit": "Влез с Парола", + "otpEmailDescription": "Код за еднократна употреба ще бъде изпратен на този имейл.", + "otpEmailSend": "Изпращане на код за еднократна употреба", + "otpEmail": "Парола за еднократна употреба (OTP)", + "otpEmailSubmit": "Изпрати OTP", + "backToEmail": "Назад към Имейл", + "noSupportKey": "Сървърът работи без поддържащ ключ. Разгледайте възможностите за подкрепа на проекта!", + "accessDenied": "Достъпът е отказан", + "accessDeniedDescription": "Не ви е разрешен достъпът до този ресурс. Ако това е грешка, моля свържете се с администратора.", + "accessTokenError": "Грешка при проверка на достъпния токен", + "accessGranted": "Достъпът е разрешен", + "accessUrlInvalid": "Невалиден URL за достъп", + "accessGrantedDescription": "Достъпът до този ресурс ви е разрешен. Пренасочване...", + "accessUrlInvalidDescription": "Този споделен URL за достъп е невалиден. Моля, свържете се със собственика на ресурса за нов URL.", + "tokenInvalid": "Невалиден токен", + "pincodeInvalid": "Невалиден код", + "passwordErrorRequestReset": "Неуспешно заявление за нулиране:", + "passwordErrorReset": "Неуспешно нулиране на паролата:", + "passwordResetSuccess": "Паролата е успешно нулирана! Връщане към вход...", + "passwordReset": "Нулиране на парола", + "passwordResetDescription": "Следвайте стъпките, за да нулирате паролата си", + "passwordResetSent": "Ще изпратим код за нулиране на паролата на този имейл адрес.", + "passwordResetCode": "Код за нулиране", + "passwordResetCodeDescription": "Проверете имейла си за код за нулиране.", + "passwordNew": "Нова парола", + "passwordNewConfirm": "Потвърдете новата парола", + "pincodeAuth": "Код на удостоверителя", + "pincodeSubmit2": "Изпрати код", + "passwordResetSubmit": "Заявка за нулиране", + "passwordBack": "Назад към Парола", + "loginBack": "Връщане към вход", + "signup": "Регистрация", + "loginStart": "Влезте, за да започнете", + "idpOidcTokenValidating": "Валидиране на OIDC токен", + "idpOidcTokenResponse": "Валидирайте отговора на OIDC токен", + "idpErrorOidcTokenValidating": "Грешка при валидиране на OIDC токен", + "idpConnectingTo": "Свързване с {name}", + "idpConnectingToDescription": "Валидиране на идентичността ви", + "idpConnectingToProcess": "Свързва се...", + "idpConnectingToFinished": "Свързано", + "idpErrorConnectingTo": "Имаше проблем със свързването към {name}. Моля, свържете се с вашия администратор.", + "idpErrorNotFound": "Не е намерен идентификационен доставчик", + "inviteInvalid": "Невалидна покана", + "inviteInvalidDescription": "Линкът към поканата е невалиден.", + "inviteErrorWrongUser": "Поканата не е за този потребител", + "inviteErrorUserNotExists": "Потребителят не съществува. Моля, създайте акаунт първо.", + "inviteErrorLoginRequired": "Трябва да сте влезли, за да приемете покана", + "inviteErrorExpired": "Може би поканата е изтекла", + "inviteErrorRevoked": "Поканата може да е била отменена", + "inviteErrorTypo": "Може би има грешка при въвеждане в линка за поканата", + "pangolinSetup": "Настройка - Pangolin", + "orgNameRequired": "Името на организацията е задължително", + "orgIdRequired": "ID на организацията е задължително", + "orgErrorCreate": "Възникна грешка при създаване на организация", + "pageNotFound": "Страницата не е намерена", + "pageNotFoundDescription": "О, не! Страницата, която търсите, не съществува.", + "overview": "Общ преглед", + "home": "Начало", + "accessControl": "Контрол на достъпа", + "settings": "Настройки", + "usersAll": "Всички потребители", + "license": "Лиценз", + "pangolinDashboard": "Табло за управление - Pangolin", + "noResults": "Няма намерени резултати.", "terabytes": "{count} TB", "gigabytes": "{count} GB", "megabytes": "{count} MB", - "tagsEntered": "Entered Tags", - "tagsEnteredDescription": "These are the tags you`ve entered.", - "tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options", - "tagsWarnInvalid": "Invalid tag as per validateTag", - "tagWarnTooShort": "Tag {tagText} is too short", - "tagWarnTooLong": "Tag {tagText} is too long", - "tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed", - "tagWarnDuplicate": "Duplicate tag {tagText} not added", - "supportKeyInvalid": "Invalid Key", - "supportKeyInvalidDescription": "Your supporter key is invalid.", - "supportKeyValid": "Valid Key", - "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", - "supportKeyErrorValidationDescription": "Failed to validate supporter key.", - "supportKey": "Support Development and Adopt a Pangolin!", - "supportKeyDescription": "Purchase a supporter key to help us continue developing Pangolin for the community. Your contribution allows us to commit more time to maintain and add new features to the application for everyone. We will never use this to paywall features. This is separate from any Commercial Edition.", - "supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!", - "supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on", - "supportKeyPurchaseLink": "our website", - "supportKeyPurchase2": "and redeem it here.", - "supportKeyLearnMore": "Learn more.", - "supportKeyOptions": "Please select the option that best suits you.", - "supportKetOptionFull": "Full Supporter", - "forWholeServer": "For the whole server", - "lifetimePurchase": "Lifetime purchase", - "supporterStatus": "Supporter status", - "buy": "Buy", - "supportKeyOptionLimited": "Limited Supporter", - "forFiveUsers": "For 5 or less users", - "supportKeyRedeem": "Redeem Supporter Key", - "supportKeyHideSevenDays": "Hide for 7 days", - "supportKeyEnter": "Enter Supporter Key", - "supportKeyEnterDescription": "Meet your very own pet Pangolin!", - "githubUsername": "GitHub Username", - "supportKeyInput": "Supporter Key", - "supportKeyBuy": "Buy Supporter Key", - "logoutError": "Error logging out", - "signingAs": "Signed in as", - "serverAdmin": "Server Admin", - "managedSelfhosted": "Managed Self-Hosted", - "otpEnable": "Enable Two-factor", - "otpDisable": "Disable Two-factor", - "logout": "Log Out", - "licenseTierProfessionalRequired": "Professional Edition Required", - "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", - "actionGetOrg": "Get Organization", - "updateOrgUser": "Update Org User", - "createOrgUser": "Create Org User", - "actionUpdateOrg": "Update Organization", - "actionUpdateUser": "Update User", - "actionGetUser": "Get User", - "actionGetOrgUser": "Get Organization User", - "actionListOrgDomains": "List Organization Domains", - "actionCreateSite": "Create Site", - "actionDeleteSite": "Delete Site", - "actionGetSite": "Get Site", - "actionListSites": "List Sites", - "actionApplyBlueprint": "Apply Blueprint", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", - "actionUpdateSite": "Update Site", - "actionListSiteRoles": "List Allowed Site Roles", - "actionCreateResource": "Create Resource", - "actionDeleteResource": "Delete Resource", - "actionGetResource": "Get Resource", - "actionListResource": "List Resources", - "actionUpdateResource": "Update Resource", - "actionListResourceUsers": "List Resource Users", - "actionSetResourceUsers": "Set Resource Users", - "actionSetAllowedResourceRoles": "Set Allowed Resource Roles", - "actionListAllowedResourceRoles": "List Allowed Resource Roles", - "actionSetResourcePassword": "Set Resource Password", - "actionSetResourcePincode": "Set Resource Pincode", - "actionSetResourceEmailWhitelist": "Set Resource Email Whitelist", - "actionGetResourceEmailWhitelist": "Get Resource Email Whitelist", - "actionCreateTarget": "Create Target", - "actionDeleteTarget": "Delete Target", - "actionGetTarget": "Get Target", - "actionListTargets": "List Targets", - "actionUpdateTarget": "Update Target", - "actionCreateRole": "Create Role", - "actionDeleteRole": "Delete Role", - "actionGetRole": "Get Role", - "actionListRole": "List Roles", - "actionUpdateRole": "Update Role", - "actionListAllowedRoleResources": "List Allowed Role Resources", - "actionInviteUser": "Invite User", - "actionRemoveUser": "Remove User", - "actionListUsers": "List Users", - "actionAddUserRole": "Add User Role", - "actionGenerateAccessToken": "Generate Access Token", - "actionDeleteAccessToken": "Delete Access Token", - "actionListAccessTokens": "List Access Tokens", - "actionCreateResourceRule": "Create Resource Rule", - "actionDeleteResourceRule": "Delete Resource Rule", - "actionListResourceRules": "List Resource Rules", - "actionUpdateResourceRule": "Update Resource Rule", - "actionListOrgs": "List Organizations", - "actionCheckOrgId": "Check ID", - "actionCreateOrg": "Create Organization", - "actionDeleteOrg": "Delete Organization", - "actionListApiKeys": "List API Keys", - "actionListApiKeyActions": "List API Key Actions", - "actionSetApiKeyActions": "Set API Key Allowed Actions", - "actionCreateApiKey": "Create API Key", - "actionDeleteApiKey": "Delete API Key", - "actionCreateIdp": "Create IDP", - "actionUpdateIdp": "Update IDP", - "actionDeleteIdp": "Delete IDP", - "actionListIdps": "List IDP", - "actionGetIdp": "Get IDP", - "actionCreateIdpOrg": "Create IDP Org Policy", - "actionDeleteIdpOrg": "Delete IDP Org Policy", - "actionListIdpOrgs": "List IDP Orgs", - "actionUpdateIdpOrg": "Update IDP Org", - "actionCreateClient": "Create Client", - "actionDeleteClient": "Delete Client", - "actionUpdateClient": "Update Client", - "actionListClients": "List Clients", - "actionGetClient": "Get Client", - "actionCreateSiteResource": "Create Site Resource", - "actionDeleteSiteResource": "Delete Site Resource", - "actionGetSiteResource": "Get Site Resource", - "actionListSiteResources": "List Site Resources", - "actionUpdateSiteResource": "Update Site Resource", - "actionListInvitations": "List Invitations", - "noneSelected": "None selected", - "orgNotFound2": "No organizations found.", - "searchProgress": "Search...", - "create": "Create", - "orgs": "Organizations", - "loginError": "An error occurred while logging in", - "passwordForgot": "Forgot your password?", - "otpAuth": "Two-Factor Authentication", - "otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.", - "otpAuthSubmit": "Submit Code", - "idpContinue": "Or continue with", - "otpAuthBack": "Back to Log In", - "navbar": "Navigation Menu", - "navbarDescription": "Main navigation menu for the application", - "navbarDocsLink": "Documentation", - "commercialEdition": "Commercial Edition", - "otpErrorEnable": "Unable to enable 2FA", - "otpErrorEnableDescription": "An error occurred while enabling 2FA", - "otpSetupCheckCode": "Please enter a 6-digit code", - "otpSetupCheckCodeRetry": "Invalid code. Please try again.", - "otpSetup": "Enable Two-factor Authentication", - "otpSetupDescription": "Secure your account with an extra layer of protection", - "otpSetupScanQr": "Scan this QR code with your authenticator app or enter the secret key manually:", - "otpSetupSecretCode": "Authenticator Code", - "otpSetupSuccess": "Two-Factor Authentication Enabled", - "otpSetupSuccessStoreBackupCodes": "Your account is now more secure. Don't forget to save your backup codes.", - "otpErrorDisable": "Unable to disable 2FA", - "otpErrorDisableDescription": "An error occurred while disabling 2FA", - "otpRemove": "Disable Two-factor Authentication", - "otpRemoveDescription": "Disable two-factor authentication for your account", - "otpRemoveSuccess": "Two-Factor Authentication Disabled", - "otpRemoveSuccessMessage": "Two-factor authentication has been disabled for your account. You can enable it again at any time.", - "otpRemoveSubmit": "Disable 2FA", - "paginator": "Page {current} of {last}", - "paginatorToFirst": "Go to first page", - "paginatorToPrevious": "Go to previous page", - "paginatorToNext": "Go to next page", - "paginatorToLast": "Go to last page", - "copyText": "Copy text", - "copyTextFailed": "Failed to copy text: ", - "copyTextClipboard": "Copy to clipboard", - "inviteErrorInvalidConfirmation": "Invalid confirmation", - "passwordRequired": "Password is required", - "allowAll": "Allow All", - "permissionsAllowAll": "Allow All Permissions", - "githubUsernameRequired": "GitHub username is required", - "supportKeyRequired": "Supporter key is required", - "passwordRequirementsChars": "Password must be at least 8 characters", - "language": "Language", - "verificationCodeRequired": "Code is required", - "userErrorNoUpdate": "No user to update", - "siteErrorNoUpdate": "No site to update", - "resourceErrorNoUpdate": "No resource to update", - "authErrorNoUpdate": "No auth info to update", - "orgErrorNoUpdate": "No org to update", - "orgErrorNoProvided": "No org provided", - "apiKeysErrorNoUpdate": "No API key to update", - "sidebarOverview": "Overview", - "sidebarHome": "Home", - "sidebarSites": "Sites", - "sidebarResources": "Resources", - "sidebarAccessControl": "Access Control", - "sidebarUsers": "Users", - "sidebarInvitations": "Invitations", - "sidebarRoles": "Roles", - "sidebarShareableLinks": "Shareable Links", - "sidebarApiKeys": "API Keys", - "sidebarSettings": "Settings", - "sidebarAllUsers": "All Users", - "sidebarIdentityProviders": "Identity Providers", - "sidebarLicense": "License", - "sidebarClients": "Clients (Beta)", - "sidebarDomains": "Domains", - "enableDockerSocket": "Enable Docker Blueprint", - "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", - "enableDockerSocketLink": "Learn More", - "viewDockerContainers": "View Docker Containers", - "containersIn": "Containers in {siteName}", - "selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.", - "containerName": "Name", - "containerImage": "Image", - "containerState": "State", - "containerNetworks": "Networks", - "containerHostnameIp": "Hostname/IP", - "containerLabels": "Labels", - "containerLabelsCount": "{count, plural, one {# label} other {# labels}}", - "containerLabelsTitle": "Container Labels", - "containerLabelEmpty": "", - "containerPorts": "Ports", - "containerPortsMore": "+{count} more", - "containerActions": "Actions", - "select": "Select", - "noContainersMatchingFilters": "No containers found matching the current filters.", - "showContainersWithoutPorts": "Show containers without ports", - "showStoppedContainers": "Show stopped containers", - "noContainersFound": "No containers found. Make sure Docker containers are running.", - "searchContainersPlaceholder": "Search across {count} containers...", - "searchResultsCount": "{count, plural, one {# result} other {# results}}", - "filters": "Filters", - "filterOptions": "Filter Options", - "filterPorts": "Ports", - "filterStopped": "Stopped", - "clearAllFilters": "Clear all filters", - "columns": "Columns", - "toggleColumns": "Toggle Columns", - "refreshContainersList": "Refresh containers list", - "searching": "Searching...", - "noContainersFoundMatching": "No containers found matching \"{filter}\".", - "light": "light", - "dark": "dark", - "system": "system", - "theme": "Theme", - "subnetRequired": "Subnet is required", - "initialSetupTitle": "Initial Server Setup", - "initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.", - "createAdminAccount": "Create Admin Account", - "setupErrorCreateAdmin": "An error occurred while creating the server admin account.", - "certificateStatus": "Certificate Status", - "loading": "Loading", - "restart": "Restart", - "domains": "Domains", - "domainsDescription": "Manage domains for your organization", - "domainsSearch": "Search domains...", - "domainAdd": "Add Domain", - "domainAddDescription": "Register a new domain with your organization", - "domainCreate": "Create Domain", - "domainCreatedDescription": "Domain created successfully", - "domainDeletedDescription": "Domain deleted successfully", - "domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?", - "domainMessageRemove": "Once removed, the domain will no longer be associated with your account.", - "domainMessageConfirm": "To confirm, please type the domain name below.", - "domainConfirmDelete": "Confirm Delete Domain", - "domainDelete": "Delete Domain", - "domain": "Domain", - "selectDomainTypeNsName": "Domain Delegation (NS)", - "selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.", - "selectDomainTypeCnameName": "Single Domain (CNAME)", - "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", - "selectDomainTypeWildcardName": "Wildcard Domain", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", - "domainDelegation": "Single Domain", - "selectType": "Select a type", - "actions": "Actions", - "refresh": "Refresh", - "refreshError": "Failed to refresh data", - "verified": "Verified", - "pending": "Pending", - "sidebarBilling": "Billing", - "billing": "Billing", - "orgBillingDescription": "Manage your billing information and subscriptions", + "tagsEntered": "Въведени тагове", + "tagsEnteredDescription": "Това са таговете, които сте въвели.", + "tagsWarnCannotBeLessThanZero": "maxTags и minTags не могат да бъдат по-малки от 0", + "tagsWarnNotAllowedAutocompleteOptions": "Таг не е разрешен, съобразно опциите за автозавършване", + "tagsWarnInvalid": "Невалиден таг според validateTag", + "tagWarnTooShort": "Таг {tagText} е твърде кратък", + "tagWarnTooLong": "Таг {tagText} е твърде дълъг", + "tagsWarnReachedMaxNumber": "Достигнат е максималния брой разрешени тагове", + "tagWarnDuplicate": "Дублиран таг {tagText} не е добавен", + "supportKeyInvalid": "Невалиден ключ", + "supportKeyInvalidDescription": "Вашият поддържащ ключ е невалиден.", + "supportKeyValid": "Валиден ключ", + "supportKeyValidDescription": "Вашият поддържащ ключ е валидиран. Благодарим за подкрепата!", + "supportKeyErrorValidationDescription": "Неуспешна валидиция на поддържащ ключ.", + "supportKey": "Подкрепете развитието и си осинови Панголин!", + "supportKeyDescription": "Купете поддържащ ключ, за да ни помогнете да продължим развитието на Pangolin за общността. Вашата помощ ни позволява да отделим повече време за поддръжка и добавяне на нови функции към приложението за всички. Ние никога няма да използваме това за заплащане на функции. Това е отделно от каквото и да е издание за комерсиални цели.", + "supportKeyPet": "Вие също ще имате възможност да осиновите и срещнете вашия собствен домашен панголин!", + "supportKeyPurchase": "Плащания се обработват през GitHub. След това можете да получите вашия ключ от", + "supportKeyPurchaseLink": "нашия уебсайт", + "supportKeyPurchase2": "и да го използвате тук.", + "supportKeyLearnMore": "Научете повече.", + "supportKeyOptions": "Изберете опцията, която най-добре съответства на вас.", + "supportKetOptionFull": "Пълна поддръжка", + "forWholeServer": "За целия сървър", + "lifetimePurchase": "Пожизнена покупка", + "supporterStatus": "Статус на поддръжника", + "buy": "Купи", + "supportKeyOptionLimited": "Ограничена поддръжка", + "forFiveUsers": "За 5 или по-малко потребители", + "supportKeyRedeem": "Изкупи поддържащ ключ", + "supportKeyHideSevenDays": "Скрий за 7 дни", + "supportKeyEnter": "Въведете поддържащ ключ", + "supportKeyEnterDescription": "Запознайте се с вашия собствен домашен панголин!", + "githubUsername": "Потребителско име в GitHub", + "supportKeyInput": "Поддържащ ключ", + "supportKeyBuy": "Купи поддържащ ключ", + "logoutError": "Грешка при излизане", + "signingAs": "Влезли сте като", + "serverAdmin": "Администратор на сървъра", + "managedSelfhosted": "Управлявано Самостоятелно-хоствано", + "otpEnable": "Включване на двуфакторен", + "otpDisable": "Изключване на двуфакторен", + "logout": "Изход", + "licenseTierProfessionalRequired": "Изисква се професионално издание", + "licenseTierProfessionalRequiredDescription": "Тази функция е достъпна само в професионалното издание.", + "actionGetOrg": "Вземете организация", + "updateOrgUser": "Актуализиране на Организационна Потребител", + "createOrgUser": "Създаване на Организационна Потребител", + "actionUpdateOrg": "Актуализиране на организацията", + "actionUpdateUser": "Актуализиране на потребител", + "actionGetUser": "Получаване на потребител", + "actionGetOrgUser": "Вземете потребител на организация", + "actionListOrgDomains": "Изброяване на домейни на организация", + "actionCreateSite": "Създаване на сайт", + "actionDeleteSite": "Изтриване на сайта", + "actionGetSite": "Вземете сайт", + "actionListSites": "Изброяване на сайтове", + "actionApplyBlueprint": "Приложи Чернова", + "setupToken": "Конфигурация на токен", + "setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.", + "setupTokenRequired": "Необходим е конфигурационен токен", + "actionUpdateSite": "Актуализиране на сайт", + "actionListSiteRoles": "Изброяване на позволените роли за сайта", + "actionCreateResource": "Създаване на ресурс", + "actionDeleteResource": "Изтриване на ресурс", + "actionGetResource": "Вземете ресурс", + "actionListResource": "Изброяване на ресурси", + "actionUpdateResource": "Актуализиране на ресурс", + "actionListResourceUsers": "Изброяване на потребители на ресурси", + "actionSetResourceUsers": "Задайте потребители на ресурси", + "actionSetAllowedResourceRoles": "Задайте позволени роли за ресурса", + "actionListAllowedResourceRoles": "Изброяване на позволени роли за ресурси", + "actionSetResourcePassword": "Задайте парола на ресурса", + "actionSetResourcePincode": "Задайте ПИН код на ресурса", + "actionSetResourceEmailWhitelist": "Задайте списък на одобрените имейл адреси за ресурса", + "actionGetResourceEmailWhitelist": "Вземете списък на одобрените имейл адреси за ресурса", + "actionCreateTarget": "Създайте цел", + "actionDeleteTarget": "Изтрийте цел", + "actionGetTarget": "Вземете цел", + "actionListTargets": "Изброяване на цели", + "actionUpdateTarget": "Актуализирайте цел", + "actionCreateRole": "Създайте роля", + "actionDeleteRole": "Изтрийте роля", + "actionGetRole": "Вземете роля", + "actionListRole": "Изброяване на роли", + "actionUpdateRole": "Актуализирайте роля", + "actionListAllowedRoleResources": "Изброяване на разрешени ресурси за роля", + "actionInviteUser": "Покани потребител", + "actionRemoveUser": "Изтрийте потребител", + "actionListUsers": "Изброяване на потребители", + "actionAddUserRole": "Добавяне на роля на потребител", + "actionGenerateAccessToken": "Генериране на токен за достъп", + "actionDeleteAccessToken": "Изтриване на токен за достъп", + "actionListAccessTokens": "Изброяване на токени за достъп", + "actionCreateResourceRule": "Създаване на правило за ресурс", + "actionDeleteResourceRule": "Изтрийте правило за ресурс", + "actionListResourceRules": "Изброяване на правила за ресурс", + "actionUpdateResourceRule": "Актуализиране на правило за ресурс", + "actionListOrgs": "Изброяване на организации", + "actionCheckOrgId": "Проверка на ID на организацията", + "actionCreateOrg": "Създаване на организация", + "actionDeleteOrg": "Изтриване на организация", + "actionListApiKeys": "Изброяване на API ключове", + "actionListApiKeyActions": "Изброяване на действия за API ключ", + "actionSetApiKeyActions": "Задайте разрешени действия за API ключ", + "actionCreateApiKey": "Създаване на API ключ", + "actionDeleteApiKey": "Изтриване на API ключ", + "actionCreateIdp": "Създаване на IdP", + "actionUpdateIdp": "Актуализиране на IdP", + "actionDeleteIdp": "Изтриване на IdP", + "actionListIdps": "Изброяване на IdP", + "actionGetIdp": "Вземете IdP", + "actionCreateIdpOrg": "Създаване на политика за IdP организация", + "actionDeleteIdpOrg": "Изтриване на политика за IdP организация", + "actionListIdpOrgs": "Изброяване на IdP организации", + "actionUpdateIdpOrg": "Актуализиране на IdP организация", + "actionCreateClient": "Създаване на клиент", + "actionDeleteClient": "Изтриване на клиент", + "actionUpdateClient": "Актуализиране на клиент", + "actionListClients": "Списък с клиенти", + "actionGetClient": "Получаване на клиент", + "actionCreateSiteResource": "Създаване на сайт ресурс", + "actionDeleteSiteResource": "Изтриване на сайт ресурс", + "actionGetSiteResource": "Получаване на сайт ресурс", + "actionListSiteResources": "Списък на ресурсите на сайта", + "actionUpdateSiteResource": "Актуализиране на сайт ресурс", + "actionListInvitations": "Списък с покани", + "noneSelected": "Нищо не е избрано", + "orgNotFound2": "Няма намерени организации.", + "searchProgress": "Търсене...", + "create": "Създаване", + "orgs": "Организации", + "loginError": "Възникна грешка при влизане", + "passwordForgot": "Забравена парола?", + "otpAuth": "Двуфакторно удостоверяване", + "otpAuthDescription": "Въведете кода от приложението за удостоверяване или един от вашите резервни кодове за еднократна употреба.", + "otpAuthSubmit": "Изпрати код", + "idpContinue": "Или продължете със", + "otpAuthBack": "Назад към Вход", + "navbar": "Навигационно меню", + "navbarDescription": "Главно навигационно меню за приложението", + "navbarDocsLink": "Документация", + "otpErrorEnable": "Не може да се активира 2FA", + "otpErrorEnableDescription": "Възникна грешка при активиране на 2FA", + "otpSetupCheckCode": "Моля, въведете 6-цифрен код", + "otpSetupCheckCodeRetry": "Невалиден код. Моля, опитайте отново.", + "otpSetup": "Активиране на двуфакторно удостоверяване", + "otpSetupDescription": "Защитете профила си с допълнителен слой защита", + "otpSetupScanQr": "Сканирайте този QR код с вашето приложение за автентикация или въведете ръчно тайния ключ:", + "otpSetupSecretCode": "Код за автентикация", + "otpSetupSuccess": "Двуфакторното удостоверяване е активирано", + "otpSetupSuccessStoreBackupCodes": "Профилът ви сега е по-сигурен. Не забравяйте да запазите резервните си кодове.", + "otpErrorDisable": "Не може да се деактивира 2FA", + "otpErrorDisableDescription": "Възникна грешка при деактивиране на 2FA", + "otpRemove": "Деактивиране на двуфакторно удостоверяване", + "otpRemoveDescription": "Деактивирайте двуфакторното удостоверяване за вашия профил", + "otpRemoveSuccess": "Двуфакторното удостоверяване е деактивирано", + "otpRemoveSuccessMessage": "Двуфакторното удостоверяване за вашия профил е деактивирано. Можете да го активирате отново по всяко време.", + "otpRemoveSubmit": "Деактивиране на 2FA", + "paginator": "Страница {current} от {last}", + "paginatorToFirst": "Отидете на първата страница", + "paginatorToPrevious": "Отидете на предишната страница", + "paginatorToNext": "Отидете на следващата страница", + "paginatorToLast": "Отидете на последната страница", + "copyText": "Копиране на текст", + "copyTextFailed": "Неуспешно копиране на текст: ", + "copyTextClipboard": "Копиране в клипборда", + "inviteErrorInvalidConfirmation": "Невалидно потвърждение", + "passwordRequired": "Паролата е задължителна", + "allowAll": "Разрешаване на всички", + "permissionsAllowAll": "Разрешаване на всички разрешения", + "githubUsernameRequired": "GitHub потребителското име е задължително", + "supportKeyRequired": "Ключът на поддръжката е задължителен", + "passwordRequirementsChars": "Паролата трябва да е поне 8 символа", + "language": "Език", + "verificationCodeRequired": "Кодът е задължителен", + "userErrorNoUpdate": "Няма потребител за актуализиране", + "siteErrorNoUpdate": "Няма сайт за актуализиране", + "resourceErrorNoUpdate": "Няма ресурс за актуализиране", + "authErrorNoUpdate": "Няма информация за удостоверяване за актуализиране", + "orgErrorNoUpdate": "Няма организация за актуализиране", + "orgErrorNoProvided": "Няма предоставена организация", + "apiKeysErrorNoUpdate": "Няма API ключ за актуализиране", + "sidebarOverview": "Общ преглед", + "sidebarHome": "Начало", + "sidebarSites": "Сайтове", + "sidebarResources": "Ресурси", + "sidebarAccessControl": "Контрол на достъпа", + "sidebarUsers": "Потребители", + "sidebarInvitations": "Покани", + "sidebarRoles": "Роли", + "sidebarShareableLinks": "Споделени връзки", + "sidebarApiKeys": "API ключове", + "sidebarSettings": "Настройки", + "sidebarAllUsers": "Всички потребители", + "sidebarIdentityProviders": "Идентификационни доставчици", + "sidebarLicense": "Лиценз", + "sidebarClients": "Clients", + "sidebarDomains": "Домейни", + "enableDockerSocket": "Активиране на Docker Чернова", + "enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.", + "enableDockerSocketLink": "Научете повече", + "viewDockerContainers": "Преглед на Docker контейнери", + "containersIn": "Контейнери в {siteName}", + "selectContainerDescription": "Изберете контейнер, който да ползвате като име на хост за целта. Натиснете порт, за да ползвате порт", + "containerName": "Име", + "containerImage": "Образ", + "containerState": "Състояние", + "containerNetworks": "Мрежи", + "containerHostnameIp": "Име на хост/IP", + "containerLabels": "Етикети", + "containerLabelsCount": "{count, plural, one {# етикет} other {# етикета}}", + "containerLabelsTitle": "Етикети на контейнера", + "containerLabelEmpty": "<празно>", + "containerPorts": "Портове", + "containerPortsMore": "+{count} още", + "containerActions": "Действия", + "select": "Избор", + "noContainersMatchingFilters": "Не са намерени контейнери, съответстващи на текущите филтри.", + "showContainersWithoutPorts": "Показване на контейнери без портове", + "showStoppedContainers": "Показване на спрени контейнери", + "noContainersFound": "Не са намерени контейнери. Уверете се, че Docker контейнерите са активирани.", + "searchContainersPlaceholder": "Търсене сред {count} контейнери...", + "searchResultsCount": "{count, plural, one {# резултат} other {# резултати}}", + "filters": "Филтри", + "filterOptions": "Опции за филтриране", + "filterPorts": "Портове", + "filterStopped": "Спрени", + "clearAllFilters": "Изчистване на всички филтри", + "columns": "Колони", + "toggleColumns": "Превключване на колони", + "refreshContainersList": "Обновяване на списъка с контейнери", + "searching": "Търсене...", + "noContainersFoundMatching": "Не са намерени контейнери, съответстващи на \"{filter}\".", + "light": "светъл", + "dark": "тъмен", + "system": "система", + "theme": "Тема", + "subnetRequired": "Необходим е подмрежа", + "initialSetupTitle": "Начална конфигурация на сървъра", + "initialSetupDescription": "Създайте администраторски акаунт на сървъра. Може да съществува само един администраторски акаунт. Винаги можете да промените тези данни по-късно.", + "createAdminAccount": "Създаване на админ акаунт", + "setupErrorCreateAdmin": "Възникна грешка при създаване на админ акаунт.", + "certificateStatus": "Статус на сертификата", + "loading": "Зареждане", + "restart": "Рестарт", + "domains": "Домейни", + "domainsDescription": "Управление на домейни за вашата организация", + "domainsSearch": "Търсене на домейни...", + "domainAdd": "Добавяне на домейн", + "domainAddDescription": "Регистриране на нов домейн с вашата организация", + "domainCreate": "Създаване на домейн", + "domainCreatedDescription": "Домейнът е създаден успешно", + "domainDeletedDescription": "Домейнът е изтрит успешно", + "domainQuestionRemove": "Сигурни ли сте, че искате да премахнете домейна {domain} от вашия профил?", + "domainMessageRemove": "След премахването, домейнът вече няма да бъде свързан с вашия профил.", + "domainMessageConfirm": "За потвърждение, моля въведете името на домейна по-долу.", + "domainConfirmDelete": "Потвърдете изтриването на домейн", + "domainDelete": "Изтриване на домейн", + "domain": "Домейн", + "selectDomainTypeNsName": "Делегация на домейни (NS)", + "selectDomainTypeNsDescription": "Този домейн и всичките му поддомейни. Ползвайте го, когато искате да контролирате цялата зона на домейна.", + "selectDomainTypeCnameName": "Единичен домейн (CNAME)", + "selectDomainTypeCnameDescription": "Само този специфичен домейн. Ползвайте го за индивидуални поддомейни или специфични домейн записи.", + "selectDomainTypeWildcardName": "Общ домейн", + "selectDomainTypeWildcardDescription": "Този домейн и неговите поддомейни.", + "domainDelegation": "Единичен домейн", + "selectType": "Изберете тип", + "actions": "Действия", + "refresh": "Обновяване", + "refreshError": "Неуспешно обновяване на данни", + "verified": "Потвърдено", + "pending": "Чакащо", + "sidebarBilling": "Фактуриране", + "billing": "Фактуриране", + "orgBillingDescription": "Управление на информацията за фактуриране и абонаментите", "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", + "pangolinHosted": "Hosted Pangolin", "fossorial": "Fossorial", - "completeAccountSetup": "Complete Account Setup", - "completeAccountSetupDescription": "Set your password to get started", - "accountSetupSent": "We'll send an account setup code to this email address.", - "accountSetupCode": "Setup Code", - "accountSetupCodeDescription": "Check your email for the setup code.", - "passwordCreate": "Create Password", - "passwordCreateConfirm": "Confirm Password", - "accountSetupSubmit": "Send Setup Code", - "completeSetup": "Complete Setup", - "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", - "documentation": "Documentation", - "saveAllSettings": "Save All Settings", - "settingsUpdated": "Settings updated", - "settingsUpdatedDescription": "All settings have been updated successfully", - "settingsErrorUpdate": "Failed to update settings", - "settingsErrorUpdateDescription": "An error occurred while updating settings", - "sidebarCollapse": "Collapse", - "sidebarExpand": "Expand", - "newtUpdateAvailable": "Update Available", - "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Domain", + "completeAccountSetup": "Завършете настройката на профила", + "completeAccountSetupDescription": "Задайте паролата си, за да започнете", + "accountSetupSent": "Ще изпратим код за настройка на профила на този адрес имейл.", + "accountSetupCode": "Код за настройка", + "accountSetupCodeDescription": "Проверете имейла си за код за настройка.", + "passwordCreate": "Създайте парола", + "passwordCreateConfirm": "Потвърждение на паролата", + "accountSetupSubmit": "Изпращане на код за настройка", + "completeSetup": "Завършване на настройката", + "accountSetupSuccess": "Настройката на профила завърши успешно! Добре дошли в Pangolin!", + "documentation": "Документация", + "saveAllSettings": "Запазване на всички настройки", + "settingsUpdated": "Настройките са обновени", + "settingsUpdatedDescription": "Всички настройки са успешно обновени", + "settingsErrorUpdate": "Неуспешно обновяване на настройките", + "settingsErrorUpdateDescription": "Възникна грешка при обновяване на настройките", + "sidebarCollapse": "Свиване", + "sidebarExpand": "Разширяване", + "newtUpdateAvailable": "Ново обновление", + "newtUpdateAvailableInfo": "Нова версия на Newt е налична. Моля, обновете до последната версия за най-добро изживяване.", + "domainPickerEnterDomain": "Домейн", "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "Enter the full domain of the resource to see available options.", - "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", - "domainPickerTabAll": "All", - "domainPickerTabOrganization": "Organization", - "domainPickerTabProvided": "Provided", + "domainPickerDescription": "Въведете пълния домейн на ресурса, за да видите наличните опции.", + "domainPickerDescriptionSaas": "Въведете пълен домейн, поддомейн или само име, за да видите наличните опции", + "domainPickerTabAll": "Всички", + "domainPickerTabOrganization": "Организация", + "domainPickerTabProvided": "Предоставен", "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", - "domainPickerOrganizationDomains": "Organization Domains", - "domainPickerProvidedDomains": "Provided Domains", - "domainPickerSubdomain": "Subdomain: {subdomain}", - "domainPickerNamespace": "Namespace: {namespace}", - "domainPickerShowMore": "Show More", - "domainNotFound": "Domain Not Found", - "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", - "failed": "Failed", - "createNewOrgDescription": "Create a new organization", - "organization": "Organization", - "port": "Port", - "securityKeyManage": "Manage Security Keys", - "securityKeyDescription": "Add or remove security keys for passwordless authentication", - "securityKeyRegister": "Register New Security Key", - "securityKeyList": "Your Security Keys", - "securityKeyNone": "No security keys registered yet", - "securityKeyNameRequired": "Name is required", - "securityKeyRemove": "Remove", - "securityKeyLastUsed": "Last used: {date}", - "securityKeyNameLabel": "Security Key Name", - "securityKeyRegisterSuccess": "Security key registered successfully", - "securityKeyRegisterError": "Failed to register security key", - "securityKeyRemoveSuccess": "Security key removed successfully", - "securityKeyRemoveError": "Failed to remove security key", - "securityKeyLoadError": "Failed to load security keys", - "securityKeyLogin": "Continue with security key", - "securityKeyAuthError": "Failed to authenticate with security key", - "securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.", - "registering": "Registering...", - "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", - "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", - "securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.", - "securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.", - "securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.", - "securityKeyUnknownError": "There was a problem using your security key. Please try again.", - "twoFactorRequired": "Two-factor authentication is required to register a security key.", - "twoFactor": "Two-Factor Authentication", - "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", - "continueToApplication": "Continue to Application", - "securityKeyAdd": "Add Security Key", - "securityKeyRegisterTitle": "Register New Security Key", - "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", - "securityKeyTwoFactorRequired": "Two-Factor Authentication Required", - "securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key", - "securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key", - "securityKeyTwoFactorCode": "Two-Factor Code", - "securityKeyRemoveTitle": "Remove Security Key", - "securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"", - "securityKeyNoKeysRegistered": "No security keys registered", - "securityKeyNoKeysDescription": "Add a security key to enhance your account security", - "createDomainRequired": "Domain is required", - "createDomainAddDnsRecords": "Add DNS Records", - "createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.", - "createDomainNsRecords": "NS Records", - "createDomainRecord": "Record", - "createDomainType": "Type:", - "createDomainName": "Name:", - "createDomainValue": "Value:", - "createDomainCnameRecords": "CNAME Records", - "createDomainARecords": "A Records", - "createDomainRecordNumber": "Record {number}", - "createDomainTxtRecords": "TXT Records", - "createDomainSaveTheseRecords": "Save These Records", - "createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.", - "createDomainDnsPropagation": "DNS Propagation", - "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", - "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "domainPickerCheckingAvailability": "Проверка на наличността...", + "domainPickerNoMatchingDomains": "Не са намерени съвпадащи домейни. Опитайте се с друг домейн или проверете настройките на домейна на вашата организация.", + "domainPickerOrganizationDomains": "Домейни на организацията", + "domainPickerProvidedDomains": "Предоставени домейни", + "domainPickerSubdomain": "Поддомейн: {subdomain}", + "domainPickerNamespace": "Име на пространство: {namespace}", + "domainPickerShowMore": "Покажи повече", + "regionSelectorTitle": "Избор на регион", + "regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.", + "regionSelectorPlaceholder": "Изберете регион", + "regionSelectorComingSoon": "Очаква се скоро", + "billingLoadingSubscription": "Зареждане на абонамент...", + "billingFreeTier": "Безплатен план", + "billingWarningOverLimit": "Предупреждение: Превишили сте една или повече лимити за използване. Вашите сайтове няма да се свържат, докато не промените абонамента си или не коригирате използването.", + "billingUsageLimitsOverview": "Преглед на лимитите за използване", + "billingMonitorUsage": "Следете използването спрямо конфигурираните лимити. Ако ви е необходимо увеличаване на лимитите, моля, свържете се с нас на support@fossorial.io.", + "billingDataUsage": "Използване на данни", + "billingOnlineTime": "Време на работа на сайта", + "billingUsers": "Активни потребители", + "billingDomains": "Активни домейни", + "billingRemoteExitNodes": "Активни самостоятелно хоствани възли", + "billingNoLimitConfigured": "Няма конфигуриран лимит", + "billingEstimatedPeriod": "Очакван период на фактуриране", + "billingIncludedUsage": "Включено използване", + "billingIncludedUsageDescription": "Използване, включено във вашия текущ абонаментен план", + "billingFreeTierIncludedUsage": "Разрешени използвания в безплатния план", + "billingIncluded": "включено", + "billingEstimatedTotal": "Очаквана сума:", + "billingNotes": "Бележки", + "billingEstimateNote": "Това е приблизителна оценка, основана на текущото ви използване.", + "billingActualChargesMayVary": "Реалните разходи могат да варират.", + "billingBilledAtEnd": "Фактурирате се в края на фактурния период.", + "billingModifySubscription": "Промяна на абонамента", + "billingStartSubscription": "Започване на абонамент", + "billingRecurringCharge": "Повтаряща се такса", + "billingManageSubscriptionSettings": "Управление на настройките и предпочитанията на абонамента ви", + "billingNoActiveSubscription": "Нямате активен абонамент. Започнете абонамента си, за да увеличите лимитите за използване.", + "billingFailedToLoadSubscription": "Грешка при зареждане на абонамент", + "billingFailedToLoadUsage": "Грешка при зареждане на използването", + "billingFailedToGetCheckoutUrl": "Неуспех при получаване на URL за плащане", + "billingPleaseTryAgainLater": "Моля, опитайте отново по-късно.", + "billingCheckoutError": "Грешка при плащане", + "billingFailedToGetPortalUrl": "Неуспех при получаване на URL на портала", + "billingPortalError": "Грешка в портала", + "billingDataUsageInfo": "Таксува се за всички данни, прехвърляни през вашите защитени тунели, когато сте свързани към облака. Това включва както входящия, така и изходящия трафик за всички ваши сайтове. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Данните не се таксуват при използване на възли.", + "billingOnlineTimeInfo": "Таксува се на база колко време вашите сайтове остават свързани с облака. Пример: 44,640 минути се равняват на един сайт работещ 24/7 за цял месец. Когато достигнете лимита си, вашите сайтове ще бъдат прекъснати, докато не надстроите плана или не намалите използването. Времето не се таксува при използване на възли.", + "billingUsersInfo": "Таксува се за всеки потребител във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни потребителски акаунти във вашата организация.", + "billingDomainInfo": "Таксува се за всеки домейн във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни домейн акаунти във вашата организация.", + "billingRemoteExitNodesInfo": "Таксува се за всеки управляван възел във вашата организация. Фактурирането се извършва ежедневно на базата на броя активни управлявани възли във вашата организация.", + "domainNotFound": "Домейнът не е намерен", + "domainNotFoundDescription": "Този ресурс е деактивиран, защото домейнът вече не съществува в нашата система. Моля, задайте нов домейн за този ресурс.", + "failed": "Неуспешно", + "createNewOrgDescription": "Създайте нова организация", + "organization": "Организация", + "port": "Порт", + "securityKeyManage": "Управление на ключове за защита", + "securityKeyDescription": "Добавяне или премахване на ключове за защита за удостоверяване без парола", + "securityKeyRegister": "Регистриране на нов ключ за защита", + "securityKeyList": "Вашите ключове за защита", + "securityKeyNone": "Все още не са регистрирани ключове за защита", + "securityKeyNameRequired": "Името е задължително", + "securityKeyRemove": "Премахване", + "securityKeyLastUsed": "Последно използван: {date}", + "securityKeyNameLabel": "Име на ключа за сигурност", + "securityKeyRegisterSuccess": "Ключът за защита е регистриран успешно", + "securityKeyRegisterError": "Неуспешна регистрация на ключ за защита", + "securityKeyRemoveSuccess": "Ключът за защита е премахнат успешно", + "securityKeyRemoveError": "Неуспешно премахване на ключ за защита", + "securityKeyLoadError": "Неуспешно зареждане на ключове за защита", + "securityKeyLogin": "Продължете с ключа за сигурност", + "securityKeyAuthError": "Неуспешно удостоверяване с ключ за сигурност", + "securityKeyRecommendation": "Регистрирайте резервен ключ за безопасност на друго устройство, за да сте сигурни, че винаги ще имате достъп до профила си", + "registering": "Регистрация...", + "securityKeyPrompt": "Моля, потвърдете самоличността си, използвайки вашия ключ за защита. Уверете се, че е свързан и готов за употреба", + "securityKeyBrowserNotSupported": "Вашият браузър не поддържа ключове за сигурност. Моля, използвайте модерен браузър като Chrome, Firefox или Safari.", + "securityKeyPermissionDenied": "Моля, позволете достъп до ключа за защита, за да продължите с влизането.", + "securityKeyRemovedTooQuickly": "Моля, дръжте ключа си за сигурност свързан, докато процеса на влизане приключи.", + "securityKeyNotSupported": "Вашият ключ за сигурност може да не е съвместим. Моля, опитайте с друг ключ.", + "securityKeyUnknownError": "Възникна проблем при използването на ключа за сигурност. Моля, опитайте отново.", + "twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.", + "twoFactor": "Двуфакторно удостоверяване", + "adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.", + "securityKeyAdd": "Добавяне на ключ за сигурност", + "securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност", + "securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате", + "securityKeyTwoFactorRequired": "Необходимо е двуфакторно удостоверяване", + "securityKeyTwoFactorDescription": "Моля, въведете вашия код за двуфакторно удостоверяване, за да регистрирате ключа за сигурност", + "securityKeyTwoFactorRemoveDescription": "Моля, въведете вашия код за двуфакторно удостоверяване, за да премахнете ключа за сигурност", + "securityKeyTwoFactorCode": "Двуфакторен код", + "securityKeyRemoveTitle": "Премахване на ключ за сигурност", + "securityKeyRemoveDescription": "Въведете паролата си, за да премахнете ключа за сигурност “{name}”", + "securityKeyNoKeysRegistered": "Няма регистрирани ключове за сигурност", + "securityKeyNoKeysDescription": "Добавяне на ключ за сигурност, за да подобрите сигурността на профила си", + "createDomainRequired": "Домейнът е задължителен", + "createDomainAddDnsRecords": "Добавяне на DNS записи", + "createDomainAddDnsRecordsDescription": "Добавете следните DNS записи на вашия домейн провайдер, за да завършите настройката.", + "createDomainNsRecords": "NS записи", + "createDomainRecord": "Запис", + "createDomainType": "Тип:", + "createDomainName": "Име:", + "createDomainValue": "Стойност:", + "createDomainCnameRecords": "CNAME записи", + "createDomainARecords": "A записи", + "createDomainRecordNumber": "Запис {number}", + "createDomainTxtRecords": "TXT записи", + "createDomainSaveTheseRecords": "Запазете тези записи", + "createDomainSaveTheseRecordsDescription": "Уверете се, че запазвате тези DNS записи, тъй като няма да ги видите отново.", + "createDomainDnsPropagation": "Разпространение на DNS", + "createDomainDnsPropagationDescription": "Промените в DNS може да отнемат време, за да се разпространят в интернет. Това може да отнеме от няколко минути до 48 часа, в зависимост от вашия DNS доставчик и TTL настройките .", + "resourcePortRequired": "Номерът на порта е задължителен за не-HTTP ресурси", + "resourcePortNotAllowed": "Номерът на порта не трябва да бъде задаван за HTTP ресурси", + "billingPricingCalculatorLink": "Калкулатор на цените", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Съгласен съм с", + "termsOfService": "условията за ползване", + "and": "и", + "privacyPolicy": "политиката за поверителност" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "siteRequired": "Изисква се сайт.", + "olmTunnel": "Olm тунел", + "olmTunnelDescription": "Използвайте Olm за клиентска свързаност", + "errorCreatingClient": "Възникна грешка при създаване на клиент", + "clientDefaultsNotFound": "Не са намерени настройки по подразбиране за клиента", + "createClient": "Създаване на клиент", + "createClientDescription": "Създайте нов клиент за свързване към вашите сайтове", + "seeAllClients": "Виж всички клиенти", + "clientInformation": "Информация за клиента", + "clientNamePlaceholder": "Име на клиента", + "address": "Адрес", + "subnetPlaceholder": "Мрежа", + "addressDescription": "Адресът, който клиентът ще използва за свързване", + "selectSites": "Избор на сайтове", + "sitesDescription": "Клиентът ще има връзка с избраните сайтове", + "clientInstallOlm": "Инсталиране на Olm", + "clientInstallOlmDescription": "Конфигурирайте Olm да работи на вашата система", + "clientOlmCredentials": "Olm Удостоверения", + "clientOlmCredentialsDescription": "Това е как Olm ще се удостоверява със сървъра", + "olmEndpoint": "Olm Ендпойнт", "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled", - "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", - "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "olmSecretKey": "Olm Тайна парола", + "clientCredentialsSave": "Запазете вашите удостоверения", + "clientCredentialsSaveDescription": "Ще можете да го видите само веднъж. Уверете се, че ще го копирате на сигурно място.", + "generalSettingsDescription": "Конфигурирайте общите настройки за този клиент", + "clientUpdated": "Клиентът актуализиран", + "clientUpdatedDescription": "Клиентът беше актуализиран.", + "clientUpdateFailed": "Актуализацията на клиента неуспешна", + "clientUpdateError": "Възникна грешка по време на актуализацията на клиента.", + "sitesFetchFailed": "Неуспешно получаване на сайтове", + "sitesFetchError": "Възникна грешка при получаването на сайтовете.", + "olmErrorFetchReleases": "Възникна грешка при получаването на Olm версиите.", + "olmErrorFetchLatest": "Възникна грешка при получаването на последната версия на Olm.", + "remoteSubnets": "Отдалечени подмрежи", + "enterCidrRange": "Въведете CIDR обхват", + "remoteSubnetsDescription": "Добавете CIDR диапазони, които могат да бъдат достъпни от този сайт отдалечено с клиенти. Използвайте формат като 10.0.0.0/24. Това се прилага САМО за VPN клиентска свързаност.", + "resourceEnableProxy": "Разрешаване на публичен прокси", + "resourceEnableProxyDescription": "Разрешете публично проксиране на този ресурс. Това позволява достъп до ресурса извън мрежата чрез облак на отворен порт. Изисква конфигурация на Traefik.", + "externalProxyEnabled": "Външен прокси разрешен", + "addNewTarget": "Добави нова цел", + "targetsList": "Списък с цели", + "advancedMode": "Разширен режим", + "targetErrorDuplicateTargetFound": "Дублирана цел намерена", + "healthCheckHealthy": "Здрав", + "healthCheckUnhealthy": "Нездрав", + "healthCheckUnknown": "Неизвестен", + "healthCheck": "Проверка на здравето", + "configureHealthCheck": "Конфигуриране на проверка на здравето", + "configureHealthCheckDescription": "Настройте мониторинг на здравето за {target}", + "enableHealthChecks": "Разрешаване на проверки на здравето", + "enableHealthChecksDescription": "Мониторинг на здравето на тази цел. Можете да наблюдавате различен краен пункт от целта, ако е необходимо.", + "healthScheme": "Метод", + "healthSelectScheme": "Избор на метод", + "healthCheckPath": "Път", + "healthHostname": "IP / Хост", + "healthPort": "Порт", + "healthCheckPathDescription": "Пътят за проверка на здравното състояние.", + "healthyIntervalSeconds": "Интервал за здраве", + "unhealthyIntervalSeconds": "Интервал за нездраве", + "IntervalSeconds": "Интервал за здраве", + "timeoutSeconds": "Време за изчакване", + "timeIsInSeconds": "Времето е в секунди", + "retryAttempts": "Опити за повторно", + "expectedResponseCodes": "Очаквани кодове за отговор", + "expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.", + "customHeaders": "Персонализирани заглавия", + "customHeadersDescription": "Add custom headers to be sent when proxying requests. One per line in the format Header-Name: value", + "headersValidationError": "Заглавията трябва да бъдат във формат: Име на заглавието: стойност.", + "saveHealthCheck": "Запазване на проверка на здравето", + "healthCheckSaved": "Проверка на здравето е запазена", + "healthCheckSavedDescription": "Конфигурацията на здравната проверка е запазена успешно", + "healthCheckError": "Грешка при проверката на здравето", + "healthCheckErrorDescription": "Възникна грешка при запазването на конфигурацията за проверка на здравето", + "healthCheckPathRequired": "Изисква се път за проверка на здравето", + "healthCheckMethodRequired": "Изисква се HTTP метод", + "healthCheckIntervalMin": "Интервалът за проверка трябва да е поне 5 секунди", + "healthCheckTimeoutMin": "Времето за изчакване трябва да е поне 1 секунда", + "healthCheckRetryMin": "Опитите за повторение трябва да са поне 1", + "httpMethod": "HTTP Метод", + "selectHttpMethod": "Изберете HTTP метод", + "domainPickerSubdomainLabel": "Поддомен", + "domainPickerBaseDomainLabel": "Основен домейн", + "domainPickerSearchDomains": "Търсене на домейни...", + "domainPickerNoDomainsFound": "Не са намерени домейни", + "domainPickerLoadingDomains": "Зареждане на домейни...", + "domainPickerSelectBaseDomain": "Изберете основен домейн...", + "domainPickerNotAvailableForCname": "Не е налично за CNAME домейни", + "domainPickerEnterSubdomainOrLeaveBlank": "Въведете поддомен или оставете празно, за да използвате основния домейн.", + "domainPickerEnterSubdomainToSearch": "Въведете поддомен, за да търсите и изберете от наличните свободни домейни.", + "domainPickerFreeDomains": "Безплатни домейни", + "domainPickerSearchForAvailableDomains": "Търсене за налични домейни", + "domainPickerNotWorkSelfHosted": "Забележка: Безплатните предоставени домейни не са налични за самостоятелно хоствани инстанции в момента.", + "resourceDomain": "Домейн", + "resourceEditDomain": "Редактиране на домейн", + "siteName": "Име на сайта", + "proxyPort": "Порт", + "resourcesTableProxyResources": "Прокси Ресурси", + "resourcesTableClientResources": "Клиентски ресурси", + "resourcesTableNoProxyResourcesFound": "Не са намерени ресурсни проксита.", + "resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.", + "resourcesTableDestination": "Дестинация", + "resourcesTableTheseResourcesForUseWith": "Тези ресурси са за използване с", + "resourcesTableClients": "Клиенти", + "resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.", + "editInternalResourceDialogEditClientResource": "Редактиране на клиентски ресурс", + "editInternalResourceDialogUpdateResourceProperties": "Актуализирайте свойствата на ресурса и конфигурацията на целите за {resourceName}.", + "editInternalResourceDialogResourceProperties": "Свойствата на ресурса", + "editInternalResourceDialogName": "Име", + "editInternalResourceDialogProtocol": "Протокол", + "editInternalResourceDialogSitePort": "Сайт Порт", + "editInternalResourceDialogTargetConfiguration": "Конфигурация на целите", + "editInternalResourceDialogCancel": "Отмяна", + "editInternalResourceDialogSaveResource": "Запазване на ресурс", + "editInternalResourceDialogSuccess": "Успех", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Вътрешният ресурс успешно актуализиран", + "editInternalResourceDialogError": "Грешка", + "editInternalResourceDialogFailedToUpdateInternalResource": "Неуспешно актуализиране на вътрешен ресурс", + "editInternalResourceDialogNameRequired": "Името е задължително", + "editInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа", + "editInternalResourceDialogProxyPortMin": "Прокси портът трябва да бъде поне 1", + "editInternalResourceDialogProxyPortMax": "Прокси портът трябва да е по-малък от 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Невалиден формат на IP адрес", + "editInternalResourceDialogDestinationPortMin": "Дестинационният порт трябва да бъде поне 1", + "editInternalResourceDialogDestinationPortMax": "Дестинационният порт трябва да е по-малък от 65536", + "createInternalResourceDialogNoSitesAvailable": "Няма достъпни сайтове", + "createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един сайт на Newt с конфигурирана мрежа, за да създадете вътрешни ресурси.", + "createInternalResourceDialogClose": "Затвори", + "createInternalResourceDialogCreateClientResource": "Създаване на клиентски ресурс", + "createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен за клиентите свързани със избрания сайт.", + "createInternalResourceDialogResourceProperties": "Свойства на ресурса", + "createInternalResourceDialogName": "Име", + "createInternalResourceDialogSite": "Сайт", + "createInternalResourceDialogSelectSite": "Изберете сайт...", + "createInternalResourceDialogSearchSites": "Търсене на сайтове...", + "createInternalResourceDialogNoSitesFound": "Не са намерени сайтове.", + "createInternalResourceDialogProtocol": "Протокол", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", + "createInternalResourceDialogSitePort": "Сайт Порт", + "createInternalResourceDialogSitePortDescription": "Използвайте този порт за достъп до ресурса на сайта при свързване с клиент.", + "createInternalResourceDialogTargetConfiguration": "Конфигурация на целите", + "createInternalResourceDialogDestinationIPDescription": "IP или хостният адрес на ресурса в мрежата на сайта.", + "createInternalResourceDialogDestinationPortDescription": "Портът на дестинационния IP, където ресурсът е достъпен.", + "createInternalResourceDialogCancel": "Отмяна", + "createInternalResourceDialogCreateResource": "Създаване на ресурс", + "createInternalResourceDialogSuccess": "Успех", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Вътрешният ресурс създаден успешно", + "createInternalResourceDialogError": "Грешка", + "createInternalResourceDialogFailedToCreateInternalResource": "Неуспешно създаване на вътрешен ресурс", + "createInternalResourceDialogNameRequired": "Името е задължително", + "createInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа", + "createInternalResourceDialogPleaseSelectSite": "Моля, изберете сайт", + "createInternalResourceDialogProxyPortMin": "Прокси портът трябва да бъде поне 1", + "createInternalResourceDialogProxyPortMax": "Прокси портът трябва да е по-малък от 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Невалиден формат на IP адрес", + "createInternalResourceDialogDestinationPortMin": "Дестинационният порт трябва да бъде поне 1", + "createInternalResourceDialogDestinationPortMax": "Дестинационният порт трябва да е по-малък от 65536", + "siteConfiguration": "Конфигурация", + "siteAcceptClientConnections": "Приемане на клиентски връзки", + "siteAcceptClientConnectionsDescription": "Позволете на други устройства да се свързват чрез този Newt инстанция като възел чрез клиенти.", + "siteAddress": "Адрес на сайта", + "siteAddressDescription": "Посочете IP адреса на хоста, към който клиентите ще се свързват. Това е вътрешният адрес на сайта в мрежата на Панголиин за адресиране от клиенти. Трябва да е в рамките на подмрежата на Организацията.", + "autoLoginExternalIdp": "Автоматично влизане с Външен IDP", + "autoLoginExternalIdpDescription": "Незабавно пренасочете потребителя към външния IDP за удостоверяване.", + "selectIdp": "Изберете IDP", + "selectIdpPlaceholder": "Изберете IDP...", + "selectIdpRequired": "Моля, изберете IDP, когато автоматичното влизане е разрешено.", + "autoLoginTitle": "Пренасочване", + "autoLoginDescription": "Пренасочване към външния доставчик на идентификационни данни за удостоверяване.", + "autoLoginProcessing": "Подготовка за удостоверяване...", + "autoLoginRedirecting": "Пренасочване към вход...", + "autoLoginError": "Грешка при автоматично влизане", + "autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.", + "autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.", + "remoteExitNodeManageRemoteExitNodes": "Отдалечени възли", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Възли", + "searchRemoteExitNodes": "Търсене на възли...", + "remoteExitNodeAdd": "Добавяне на възел", + "remoteExitNodeErrorDelete": "Грешка при изтриване на възел", + "remoteExitNodeQuestionRemove": "Сигурни ли сте, че искате да премахнете възела {selectedNode} от организацията?", + "remoteExitNodeMessageRemove": "След премахване, възелът вече няма да бъде достъпен.", + "remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.", + "remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)", + "remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)", + "sidebarRemoteExitNodes": "Отдалечени възли", + "remoteExitNodeCreate": { + "title": "Създаване на възел", + "description": "Създайте нов възел, за да разширите мрежовата си свързаност", + "viewAllButton": "Вижте всички възли", + "strategy": { + "title": "Стратегия на създаване", + "description": "Изберете това, за да конфигурирате ръчно възела си или да създадете нови кредити.", + "adopt": { + "title": "Осиновете възел", + "description": "Изберете това, ако вече имате кредити за възела." + }, + "generate": { + "title": "Генериране на ключове", + "description": "Изберете това, ако искате да генерирате нови ключове за възела" + } + }, + "adopt": { + "title": "Осиновяване на съществуващ възел", + "description": "Въведете данните на съществуващия възел, който искате да осиновите", + "nodeIdLabel": "ID на възела", + "nodeIdDescription": "ID на съществуващия възел, който искате да осиновите", + "secretLabel": "Секретен", + "secretDescription": "Секретният ключ на съществуващия възел", + "submitButton": "Осиновете възела" + }, + "generate": { + "title": "Генерирани кредити", + "description": "Използвайте тези генерирани кредити, за да конфигурирате възела си", + "nodeIdTitle": "ID на възела", + "secretTitle": "Секретен", + "saveCredentialsTitle": "Добавете кредити към конфигурацията", + "saveCredentialsDescription": "Добавете тези кредити към конфигурационния файл на вашия самостоятелно хостван Pangolin възел, за да завършите връзката.", + "submitButton": "Създаване на възел" + }, + "validation": { + "adoptRequired": "ID на възела и секрет са необходими при осиновяване на съществуващ възел" + }, + "errors": { + "loadDefaultsFailed": "Грешка при зареждане на подразбирани настройки", + "defaultsNotLoaded": "Подразбирани настройки не са заредени", + "createFailed": "Грешка при създаване на възел" + }, + "success": { + "created": "Възелът е създаден успешно" + } + }, + "remoteExitNodeSelection": "Избор на възел", + "remoteExitNodeSelectionDescription": "Изберете възел, през който да пренасочвате трафика за местния сайт", + "remoteExitNodeRequired": "Необходимо е да бъде избран възел за местни сайтове", + "noRemoteExitNodesAvailable": "Няма налични възли", + "noRemoteExitNodesAvailableDescription": "Няма налични възли за тази организация. Първо създайте възел, за да използвате местни сайтове.", + "exitNode": "Изходен възел", + "country": "Държава", + "rulesMatchCountry": "Понастоящем на базата на изходния IP", "managedSelfHosted": { - "title": "Managed Self-Hosted", - "description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles", - "introTitle": "Managed Self-Hosted Pangolin", - "introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.", - "introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:", + "title": "Управлявано Самостоятелно-хоствано", + "description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри", + "introTitle": "Управлявано Самостоятелно-хостван Панголиин", + "introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.", + "introDetail": "С тази опция все още управлявате свой собствен Панголиин възел — вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:", "benefitSimplerOperations": { - "title": "Simpler operations", - "description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box." + "title": "По-прости операции", + "description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало." }, "benefitAutomaticUpdates": { - "title": "Automatic updates", - "description": "The cloud dashboard evolves quickly, so you get new features and bug fixes without having to manually pull new containers every time." + "title": "Автоматични актуализации", + "description": "Облачният панел за контрол се развива бързо, така че получавате нови функции и корекции на грешки без да се налага да извличате нови контейнери всеки път." }, "benefitLessMaintenance": { - "title": "Less maintenance", - "description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud." + "title": "По-малко поддръжка", + "description": "Няма миграции на база от данни, резервни копия или допълнителна инфраструктура за управление. Ние се грижим за това в облака." }, "benefitCloudFailover": { - "title": "Cloud failover", - "description": "If your node goes down, your tunnels can temporarily fail over to our cloud points of presence until you bring it back online." + "title": "Облачно преобръщане", + "description": "Ако вашият възел спре да работи, вашите тунели могат временно да преориентират към нашите облачни точки, докато не го възстановите." }, "benefitHighAvailability": { - "title": "High availability (PoPs)", - "description": "You can also attach multiple nodes to your account for redundancy and better performance." + "title": "Висока наличност (PoPs)", + "description": "Можете също така да прикрепите множество възли към вашия акаунт за резервно копиране и по-добра производителност." }, "benefitFutureEnhancements": { - "title": "Future enhancements", - "description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust." + "title": "Бъдещи подобрения", + "description": "Планираме да добавим още аналитични, алармиращи и управителни инструменти, за да направим вашето внедряване още по-здраво." }, "docsAlert": { - "text": "Learn more about the Managed Self-Hosted option in our", - "documentation": "documentation" + "text": "Научете повече за Управляваното Самостоятелно-хоствано опцията в нашата", + "documentation": "документация" }, - "convertButton": "Convert This Node to Managed Self-Hosted" + "convertButton": "Конвертирайте този възел в Управлявано Самостоятелно-хоствано" }, - "internationaldomaindetected": "International Domain Detected", - "willbestoredas": "Will be stored as:", - "idpGoogleDescription": "Google OAuth2/OIDC provider", - "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Custom Headers", - "headersValidationError": "Headers must be in the format: Header-Name: value.", - "domainPickerProvidedDomain": "Provided Domain", - "domainPickerFreeProvidedDomain": "Free Provided Domain", - "domainPickerVerified": "Verified", - "domainPickerUnverified": "Unverified", - "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", - "domainPickerError": "Error", - "domainPickerErrorLoadDomains": "Failed to load organization domains", - "domainPickerErrorCheckAvailability": "Failed to check domain availability", - "domainPickerInvalidSubdomain": "Invalid subdomain", - "domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.", - "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", - "domainPickerSubdomainSanitized": "Subdomain sanitized", - "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", - "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Edit file: docker-compose.yml" + "internationaldomaindetected": "Открит международен домейн", + "willbestoredas": "Ще бъде съхранено като:", + "roleMappingDescription": "Определете как се разпределят ролите на потребителите при вписване, когато е активирано автоматично предоставяне.", + "selectRole": "Избор на роля", + "roleMappingExpression": "Израз", + "selectRolePlaceholder": "Избор на роля", + "selectRoleDescription": "Изберете роля за присвояване на всички потребители от този доставчик на идентичност", + "roleMappingExpressionDescription": "Въведете израз JMESPath, за да извлечете информация за ролята от ID токена", + "idpTenantIdRequired": "Изисква се идентификационен номер на наемателя", + "invalidValue": "Невалидна стойност", + "idpTypeLabel": "Тип на доставчика на идентичност", + "roleMappingExpressionPlaceholder": "напр.: contains(groups, 'admin') && 'Admin' || 'Member'", + "idpGoogleConfiguration": "Конфигурация на Google", + "idpGoogleConfigurationDescription": "Конфигурирайте своите Google OAuth2 кредити", + "idpGoogleClientIdDescription": "Вашият Google OAuth2 клиентски ID", + "idpGoogleClientSecretDescription": "Вашият Google OAuth2 клиентски секрет", + "idpAzureConfiguration": "Конфигурация на Azure Entra ID", + "idpAzureConfigurationDescription": "Конфигурирайте своите Azure Entra ID OAuth2 кредити", + "idpTenantId": "Идентификационен номер на наемателя", + "idpTenantIdPlaceholder": "вашият идентификационен номер на наемателя", + "idpAzureTenantIdDescription": "Вашият Azure идентификационен номер на наемателя (намира се в преглед на Azure Active Directory)", + "idpAzureClientIdDescription": "Вашият Azure клиентски идентификационен номер за приложението", + "idpAzureClientSecretDescription": "Вашият Azure клиентски секрет за приложението", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Конфигурация на Google", + "idpAzureConfigurationTitle": "Конфигурация на Azure Entra ID", + "idpTenantIdLabel": "Идентификационен номер на наемателя", + "idpAzureClientIdDescription2": "Вашият Azure клиентски идентификационен номер за приложението", + "idpAzureClientSecretDescription2": "Вашият Azure клиентски секрет за приложението", + "idpGoogleDescription": "Google OAuth2/OIDC доставчик", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC доставчик", + "subnet": "Подмрежа", + "subnetDescription": "Подмрежата за конфигурацията на мрежата на тази организация.", + "authPage": "Страница за удостоверяване", + "authPageDescription": "Конфигурирайте страницата за удостоверяване на вашата организация", + "authPageDomain": "Домен на страницата за удостоверяване", + "noDomainSet": "Няма зададен домейн", + "changeDomain": "Смяна на домейн", + "selectDomain": "Избор на домейн", + "restartCertificate": "Рестартиране на сертификат", + "editAuthPageDomain": "Редактиране на домейна на страницата за удостоверяване", + "setAuthPageDomain": "Задаване на домейн на страницата за удостоверяване", + "failedToFetchCertificate": "Неуспех при извличане на сертификат", + "failedToRestartCertificate": "Неуспех при рестартиране на сертификат", + "addDomainToEnableCustomAuthPages": "Добавете домейн, за да активирате персонализирани страници за удостоверяване за вашата организация", + "selectDomainForOrgAuthPage": "Изберете домейн за страницата за удостоверяване на организацията", + "domainPickerProvidedDomain": "Предоставен домейн", + "domainPickerFreeProvidedDomain": "Безплатен предоставен домейн", + "domainPickerVerified": "Проверено", + "domainPickerUnverified": "Непроверено", + "domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.", + "domainPickerError": "Грешка", + "domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията", + "domainPickerErrorCheckAvailability": "Неуспешна проверка на наличността на домейни", + "domainPickerInvalidSubdomain": "Невалиден поддомен", + "domainPickerInvalidSubdomainRemoved": "Входът \"{sub}\" беше премахнат, защото не е валиден.", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не може да се направи валиден за {domain}.", + "domainPickerSubdomainSanitized": "Поддомен пречистен", + "domainPickerSubdomainCorrected": "\"{sub}\" беше коригиран на \"{sanitized}\"", + "orgAuthSignInTitle": "Впишете се във вашата организация", + "orgAuthChooseIdpDescription": "Изберете своя доставчик на идентичност, за да продължите", + "orgAuthNoIdpConfigured": "Тази организация няма конфигурирани доставчици на идентичност. Можете да влезете с вашата Pangolin идентичност.", + "orgAuthSignInWithPangolin": "Впишете се с Pangolin", + "subscriptionRequiredToUse": "Необходим е абонамент, за да използвате тази функция.", + "idpDisabled": "Доставчиците на идентичност са деактивирани.", + "orgAuthPageDisabled": "Страницата за удостоверяване на организацията е деактивирана.", + "domainRestartedDescription": "Проверка на домейна е рестартирана успешно", + "resourceAddEntrypointsEditFile": "Редактиране на файл: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Редактиране на файл: docker-compose.yml", + "emailVerificationRequired": "Потвърждението на Email е необходимо. Моля, влезте отново чрез {dashboardUrl}/auth/login, за да завършите тази стъпка. След това, върнете се тук.", + "twoFactorSetupRequired": "Необходима е настройка на двуфакторно удостоверяване. Моля, влезте отново чрез {dashboardUrl}/auth/login, за да завършите тази стъпка. След това, върнете се тук.", + "authPageErrorUpdateMessage": "Възникна грешка при актуализирането на настройките на страницата за удостоверяване", + "authPageUpdated": "Страницата за удостоверяване е актуализирана успешно", + "healthCheckNotAvailable": "Локална", + "rewritePath": "Пренапиши път", + "rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта.", + "continueToApplication": "Продължете до приложението", + "checkingInvite": "Проверка на поканата", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Премахване на автентикация в заглавката", + "resourceHeaderAuthRemoveDescription": "Автентикацията в заглавката беше премахната успешно.", + "resourceErrorHeaderAuthRemove": "Неуспешно премахване на автентикация в заглавката", + "resourceErrorHeaderAuthRemoveDescription": "Не беше възможно премахването на автентикацията в заглавката за ресурса.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Неуспешно задаване на автентикация в заглавката", + "resourceErrorHeaderAuthSetupDescription": "Не беше възможно задаването на автентикация в заглавката за ресурса.", + "resourceHeaderAuthSetup": "Автентикацията в заглавката беше зададена успешно", + "resourceHeaderAuthSetupDescription": "Автентикацията в заглавката беше успешно зададена.", + "resourceHeaderAuthSetupTitle": "Задаване на автентикация в заглавката", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Задаване на автентикация в заглавката", + "actionSetResourceHeaderAuth": "Задаване на автентикация в заглавката", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Приоритет", + "priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 7b391431..2eddea2a 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -96,7 +96,7 @@ "siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.", "siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", "siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.", - "siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Zobrazit všechny lokality", "siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě", "siteNewtCredentials": "Přihlašovací údaje Newt", @@ -157,9 +157,9 @@ "resourceMessageConfirm": "Pro potvrzení zadejte prosím název zdroje.", "resourceQuestionRemove": "Opravdu chcete odstranit zdroj {selectedResource} z organizace?", "resourceHTTP": "Zdroj HTTPS", - "resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.", - "resourceRaw": "Raw TCP/UDP Resource", - "resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.", + "resourceHTTPDescription": "Požadavky na proxy pro vaši 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.", "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", @@ -168,6 +168,9 @@ "siteSelect": "Vybrat lokalitu", "siteSearch": "Hledat lokalitu", "siteNotFound": "Nebyla nalezena žádná lokalita.", + "selectCountry": "Vyberte zemi", + "searchCountries": "Hledat země...", + "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", @@ -188,1336 +191,1707 @@ "resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační snippety pro nastavení TCP/UDP zdroje", "resourceAddEntrypoints": "Traefik: Přidat vstupní body", "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", - "resourceLearnRaw": "Learn how to configure TCP/UDP resources", - "resourceBack": "Back to Resources", - "resourceGoTo": "Go to Resource", - "resourceDelete": "Delete Resource", - "resourceDeleteConfirm": "Confirm Delete Resource", - "visibility": "Visibility", - "enabled": "Enabled", - "disabled": "Disabled", - "general": "General", - "generalSettings": "General Settings", - "proxy": "Proxy", - "internal": "Internal", - "rules": "Rules", - "resourceSettingDescription": "Configure the settings on your resource", - "resourceSetting": "{resourceName} Settings", - "alwaysAllow": "Always Allow", - "alwaysDeny": "Always Deny", - "passToAuth": "Pass to Auth", - "orgSettingsDescription": "Configure your organization's general settings", - "orgGeneralSettings": "Organization Settings", - "orgGeneralSettingsDescription": "Manage your organization details and configuration", - "saveGeneralSettings": "Save General Settings", - "saveSettings": "Save Settings", - "orgDangerZone": "Danger Zone", - "orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.", - "orgDelete": "Delete Organization", - "orgDeleteConfirm": "Confirm Delete Organization", - "orgMessageRemove": "This action is irreversible and will delete all associated data.", - "orgMessageConfirm": "To confirm, please type the name of the organization below.", - "orgQuestionRemove": "Are you sure you want to remove the organization {selectedOrg}?", - "orgUpdated": "Organization updated", - "orgUpdatedDescription": "The organization has been updated.", - "orgErrorUpdate": "Failed to update organization", - "orgErrorUpdateMessage": "An error occurred while updating the organization.", - "orgErrorFetch": "Failed to fetch organizations", - "orgErrorFetchMessage": "An error occurred while listing your organizations", - "orgErrorDelete": "Failed to delete organization", - "orgErrorDeleteMessage": "An error occurred while deleting the organization.", - "orgDeleted": "Organization deleted", - "orgDeletedMessage": "The organization and its data has been deleted.", - "orgMissing": "Organization ID Missing", - "orgMissingMessage": "Unable to regenerate invitation without an organization ID.", - "accessUsersManage": "Manage Users", - "accessUsersDescription": "Invite users and add them to roles to manage access to your organization", - "accessUsersSearch": "Search users...", - "accessUserCreate": "Create User", - "accessUserRemove": "Remove User", - "username": "Username", - "identityProvider": "Identity Provider", + "resourceLearnRaw": "Naučte se konfigurovat zdroje TCP/UDP", + "resourceBack": "Zpět na zdroje", + "resourceGoTo": "Přejít na dokument", + "resourceDelete": "Odstranit dokument", + "resourceDeleteConfirm": "Potvrdit odstranění dokumentu", + "visibility": "Viditelnost", + "enabled": "Povoleno", + "disabled": "Zakázáno", + "general": "Obecná ustanovení", + "generalSettings": "Obecná nastavení", + "proxy": "Proxy server", + "internal": "Interní", + "rules": "Pravidla", + "resourceSettingDescription": "Konfigurace nastavení na vašem zdroji", + "resourceSetting": "Nastavení {resourceName}", + "alwaysAllow": "Vždy povolit", + "alwaysDeny": "Vždy zakázat", + "passToAuth": "Předat k ověření", + "orgSettingsDescription": "Konfigurace obecných nastavení vaší organizace", + "orgGeneralSettings": "Nastavení organizace", + "orgGeneralSettingsDescription": "Spravujte údaje a konfiguraci vaší organizace", + "saveGeneralSettings": "Uložit obecné nastavení", + "saveSettings": "Uložit nastavení", + "orgDangerZone": "Nebezpečná zóna", + "orgDangerZoneDescription": "Jakmile smažete tento org, nic se nevrátí. Buďte si jistí.", + "orgDelete": "Odstranit organizaci", + "orgDeleteConfirm": "Potvrdit odstranění organizace", + "orgMessageRemove": "Tato akce je nevratná a odstraní všechna související data.", + "orgMessageConfirm": "Pro potvrzení zadejte níže uvedený název organizace.", + "orgQuestionRemove": "Opravdu chcete odstranit organizaci {selectedOrg}?", + "orgUpdated": "Organizace byla aktualizována", + "orgUpdatedDescription": "Organizace byla aktualizována.", + "orgErrorUpdate": "Aktualizace organizace se nezdařila", + "orgErrorUpdateMessage": "Došlo k chybě při aktualizaci organizace.", + "orgErrorFetch": "Nepodařilo se načíst organizace", + "orgErrorFetchMessage": "Došlo k chybě při výpisu vašich organizací", + "orgErrorDelete": "Nepodařilo se odstranit organizaci", + "orgErrorDeleteMessage": "Došlo k chybě při odstraňování organizace.", + "orgDeleted": "Organizace odstraněna", + "orgDeletedMessage": "Organizace a její data byla smazána.", + "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", + "accessUsersSearch": "Hledat uživatele...", + "accessUserCreate": "Vytvořit uživatele", + "accessUserRemove": "Odstranit uživatele", + "username": "Uživatelské jméno", + "identityProvider": "Poskytovatel identity", "role": "Role", - "nameRequired": "Name is required", - "accessRolesManage": "Manage Roles", - "accessRolesDescription": "Configure roles to manage access to your organization", - "accessRolesSearch": "Search roles...", - "accessRolesAdd": "Add Role", - "accessRoleDelete": "Delete Role", - "description": "Description", - "inviteTitle": "Open Invitations", - "inviteDescription": "Manage your invitations to other users", - "inviteSearch": "Search invitations...", - "minutes": "Minutes", - "hours": "Hours", - "days": "Days", - "weeks": "Weeks", - "months": "Months", - "years": "Years", - "day": "{count, plural, one {# day} other {# days}}", - "apiKeysTitle": "API Key Information", - "apiKeysConfirmCopy2": "You must confirm that you have copied the API key.", - "apiKeysErrorCreate": "Error creating API key", - "apiKeysErrorSetPermission": "Error setting permissions", - "apiKeysCreate": "Generate API Key", - "apiKeysCreateDescription": "Generate a new API key for your organization", - "apiKeysGeneralSettings": "Permissions", - "apiKeysGeneralSettingsDescription": "Determine what this API key can do", - "apiKeysList": "Your API Key", - "apiKeysSave": "Save Your API Key", - "apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "apiKeysInfo": "Your API key is:", - "apiKeysConfirmCopy": "I have copied the API key", - "generate": "Generate", - "done": "Done", - "apiKeysSeeAll": "See All API Keys", - "apiKeysPermissionsErrorLoadingActions": "Error loading API key actions", - "apiKeysPermissionsErrorUpdate": "Error setting permissions", - "apiKeysPermissionsUpdated": "Permissions updated", - "apiKeysPermissionsUpdatedDescription": "The permissions have been updated.", - "apiKeysPermissionsGeneralSettings": "Permissions", - "apiKeysPermissionsGeneralSettingsDescription": "Determine what this API key can do", - "apiKeysPermissionsSave": "Save Permissions", - "apiKeysPermissionsTitle": "Permissions", - "apiKeys": "API Keys", - "searchApiKeys": "Search API keys...", - "apiKeysAdd": "Generate API Key", - "apiKeysErrorDelete": "Error deleting API key", - "apiKeysErrorDeleteMessage": "Error deleting API key", - "apiKeysQuestionRemove": "Are you sure you want to remove the API key {selectedApiKey} from the organization?", - "apiKeysMessageRemove": "Once removed, the API key will no longer be able to be used.", - "apiKeysMessageConfirm": "To confirm, please type the name of the API key below.", - "apiKeysDeleteConfirm": "Confirm Delete API Key", - "apiKeysDelete": "Delete API Key", - "apiKeysManage": "Manage API Keys", - "apiKeysDescription": "API keys are used to authenticate with the integration API", - "apiKeysSettings": "{apiKeyName} Settings", - "userTitle": "Manage All Users", - "userDescription": "View and manage all users in the system", - "userAbount": "About User Management", - "userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.", - "userServer": "Server Users", - "userSearch": "Search server users...", - "userErrorDelete": "Error deleting user", - "userDeleteConfirm": "Confirm Delete User", - "userDeleteServer": "Delete User from Server", - "userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.", - "userMessageConfirm": "To confirm, please type the name of the user below.", - "userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?", - "licenseKey": "License Key", + "nameRequired": "Název je povinný", + "accessRolesManage": "Spravovat role", + "accessRolesDescription": "Konfigurace rolí pro správu přístupu do vaší organizace", + "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", + "inviteSearch": "Hledat pozvánky...", + "minutes": "Zápis z jednání", + "hours": "Hodiny", + "days": "Dny", + "weeks": "Týdny", + "months": "Měsíce", + "years": "Roky", + "day": "{count, plural, one {# den} other {# dní}}", + "apiKeysTitle": "Informace API klíče", + "apiKeysConfirmCopy2": "Musíte potvrdit, že jste zkopírovali API klíč.", + "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", + "apiKeysGeneralSettings": "Práva", + "apiKeysGeneralSettingsDescription": "Určete, co může tento API klíč udělat", + "apiKeysList": "Váš API klíč", + "apiKeysSave": "Uložit váš 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:", + "apiKeysConfirmCopy": "Kopíroval jsem API klíč", + "generate": "Generovat", + "done": "Hotovo", + "apiKeysSeeAll": "Zobrazit všechny API klíče", + "apiKeysPermissionsErrorLoadingActions": "Chyba při načítání akcí API klíče", + "apiKeysPermissionsErrorUpdate": "Chyba nastavení oprávnění", + "apiKeysPermissionsUpdated": "Oprávnění byla aktualizována", + "apiKeysPermissionsUpdatedDescription": "Oprávnění byla aktualizována.", + "apiKeysPermissionsGeneralSettings": "Práva", + "apiKeysPermissionsGeneralSettingsDescription": "Určete, co může tento API klíč udělat", + "apiKeysPermissionsSave": "Uložit oprávnění", + "apiKeysPermissionsTitle": "Práva", + "apiKeys": "API klíče", + "searchApiKeys": "Hledat API klíče...", + "apiKeysAdd": "Generovat API klíč", + "apiKeysErrorDelete": "Chyba při odstraňování API klíče", + "apiKeysErrorDeleteMessage": "Chyba při odstraňování API klíče", + "apiKeysQuestionRemove": "Opravdu chcete odstranit API klíč {selectedApiKey} z organizace?", + "apiKeysMessageRemove": "Po odstranění klíče API již nebude možné použít.", + "apiKeysMessageConfirm": "Pro potvrzení zadejte název klíče API.", + "apiKeysDeleteConfirm": "Potvrdit odstranění API klíče", + "apiKeysDelete": "Odstranit klíč API", + "apiKeysManage": "Správa API klíčů", + "apiKeysDescription": "API klíče se používají k ověření s integračním API", + "apiKeysSettings": "Nastavení {apiKeyName}", + "userTitle": "Spravovat všechny uživatele", + "userDescription": "Zobrazit a spravovat všechny uživatele v systému", + "userAbount": "O správě uživatelů", + "userAbountDescription": "Tato tabulka zobrazuje všechny root uživatelské objekty v systému. Každý uživatel může patřit do více organizací. Odstranění uživatele z organizace neodstraní jeho kořenový uživatelský objekt - zůstanou v systému. Pro úplné odstranění uživatele ze systému musíte odstranit jejich kořenový uživatelský objekt pomocí smazané akce v této tabulce.", + "userServer": "Uživatelé serveru", + "userSearch": "Hledat uživatele serveru...", + "userErrorDelete": "Chyba při odstraňování uživatele", + "userDeleteConfirm": "Potvrdit odstranění uživatele", + "userDeleteServer": "Odstranit uživatele ze serveru", + "userMessageRemove": "Uživatel bude odstraněn ze všech organizací a bude zcela odstraněn ze serveru.", + "userMessageConfirm": "Pro potvrzení zadejte níže uvedené jméno uživatele.", + "userQuestionRemove": "Opravdu chcete trvale odstranit {selectedUser} ze serveru?", + "licenseKey": "Licenční klíč", "valid": "Valid", - "numberOfSites": "Number of Sites", - "licenseKeySearch": "Search license keys...", - "licenseKeyAdd": "Add License Key", - "type": "Type", - "licenseKeyRequired": "License key is required", - "licenseTermsAgree": "You must agree to the license terms", - "licenseErrorKeyLoad": "Failed to load license keys", - "licenseErrorKeyLoadDescription": "An error occurred loading license keys.", - "licenseErrorKeyDelete": "Failed to delete license key", - "licenseErrorKeyDeleteDescription": "An error occurred deleting license key.", - "licenseKeyDeleted": "License key deleted", - "licenseKeyDeletedDescription": "The license key has been deleted.", - "licenseErrorKeyActivate": "Failed to activate license key", - "licenseErrorKeyActivateDescription": "An error occurred while activating the license key.", - "licenseAbout": "About Licensing", - "communityEdition": "Community Edition", - "licenseAboutDescription": "This is for business and enterprise users who are using Pangolin in a commercial environment. If you are using Pangolin for personal use, you can ignore this section.", - "licenseKeyActivated": "License key activated", - "licenseKeyActivatedDescription": "The license key has been successfully activated.", - "licenseErrorKeyRecheck": "Failed to recheck license keys", - "licenseErrorKeyRecheckDescription": "An error occurred rechecking license keys.", - "licenseErrorKeyRechecked": "License keys rechecked", - "licenseErrorKeyRecheckedDescription": "All license keys have been rechecked", - "licenseActivateKey": "Activate License Key", - "licenseActivateKeyDescription": "Enter a license key to activate it.", - "licenseActivate": "Activate License", - "licenseAgreement": "By checking this box, you confirm that you have read and agree to the license terms corresponding to the tier associated with your license key.", - "fossorialLicense": "View Fossorial Commercial License & Subscription Terms", - "licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.", - "licenseMessageConfirm": "To confirm, please type the license key below.", - "licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?", - "licenseKeyDelete": "Delete License Key", - "licenseKeyDeleteConfirm": "Confirm Delete License Key", - "licenseTitle": "Manage License Status", - "licenseTitleDescription": "View and manage license keys in the system", - "licenseHost": "Host License", - "licenseHostDescription": "Manage the main license key for the host.", - "licensedNot": "Not Licensed", - "hostId": "Host ID", - "licenseReckeckAll": "Recheck All Keys", - "licenseSiteUsage": "Sites Usage", - "licenseSiteUsageDecsription": "View the number of sites using this license.", - "licenseNoSiteLimit": "There is no limit on the number of sites using an unlicensed host.", - "licensePurchase": "Purchase License", - "licensePurchaseSites": "Purchase Additional Sites", - "licenseSitesUsedMax": "{usedSites} of {maxSites} sites used", - "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} in system.", - "licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}", - "licenseFee": "License fee", - "licensePriceSite": "Price per site", - "total": "Total", - "licenseContinuePayment": "Continue to Payment", - "pricingPage": "pricing page", - "pricingPortal": "See Purchase Portal", - "licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ", - "invite": "Invitations", - "inviteRegenerate": "Regenerate Invitation", - "inviteRegenerateDescription": "Revoke previous invitation and create a new one", - "inviteRemove": "Remove Invitation", - "inviteRemoveError": "Failed to remove invitation", - "inviteRemoveErrorDescription": "An error occurred while removing the invitation.", - "inviteRemoved": "Invitation removed", - "inviteRemovedDescription": "The invitation for {email} has been removed.", - "inviteQuestionRemove": "Are you sure you want to remove the invitation {email}?", - "inviteMessageRemove": "Once removed, this invitation will no longer be valid. You can always re-invite the user later.", - "inviteMessageConfirm": "To confirm, please type the email address of the invitation below.", - "inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for {email}? This will revoke the previous invitation.", - "inviteRemoveConfirm": "Confirm Remove Invitation", - "inviteRegenerated": "Invitation Regenerated", - "inviteSent": "A new invitation has been sent to {email}.", - "inviteSentEmail": "Send email notification to the user", - "inviteGenerate": "A new invitation has been generated for {email}.", + "numberOfSites": "Počet stránek", + "licenseKeySearch": "Hledat licenční klíče...", + "licenseKeyAdd": "Přidat licenční klíč", + "type": "Typ", + "licenseKeyRequired": "Je vyžadován licenční klíč", + "licenseTermsAgree": "Musíte souhlasit s podmínkami licence", + "licenseErrorKeyLoad": "Nepodařilo se načíst licenční klíče", + "licenseErrorKeyLoadDescription": "Došlo k chybě při načítání licenčních klíčů.", + "licenseErrorKeyDelete": "Nepodařilo se odstranit licenční klíč", + "licenseErrorKeyDeleteDescription": "Došlo k chybě při odstraňování licenčního klíče.", + "licenseKeyDeleted": "Licenční klíč byl smazán", + "licenseKeyDeletedDescription": "Licenční klíč byl odstraněn.", + "licenseErrorKeyActivate": "Nepodařilo se aktivovat licenční klíč", + "licenseErrorKeyActivateDescription": "Došlo k chybě při aktivaci licenčního klíče.", + "licenseAbout": "O licencích", + "communityEdition": "Komunitní edice", + "licenseAboutDescription": "To je pro obchodní a podnikové uživatele, kteří používají Pangolin v komerčním prostředí. Pokud používáte Pangolin pro osobní použití, můžete tuto sekci ignorovat.", + "licenseKeyActivated": "Licenční klíč aktivován", + "licenseKeyActivatedDescription": "Licenční klíč byl úspěšně aktivován.", + "licenseErrorKeyRecheck": "Nepodařilo se znovu zkontrolovat licenční klíče", + "licenseErrorKeyRecheckDescription": "Došlo k chybě při opětovné kontrole licenčních klíčů.", + "licenseErrorKeyRechecked": "Licenční klíče překontrolovány", + "licenseErrorKeyRecheckedDescription": "Všechny licenční klíče byly znovu zkontrolovány", + "licenseActivateKey": "Aktivovat licenční klíč", + "licenseActivateKeyDescription": "Zadejte licenční klíč pro jeho aktivaci.", + "licenseActivate": "Aktivovat licenci", + "licenseAgreement": "Zaškrtnutím tohoto políčka potvrdíte, že jste si přečetli licenční podmínky odpovídající úrovni přiřazené k vašemu licenčnímu klíči a souhlasíte s nimi.", + "fossorialLicense": "Zobrazit Fossorial Commercial License & Subscription terms", + "licenseMessageRemove": "Tímto odstraníte licenční klíč a všechna s ním spojená oprávnění, která mu byla udělena.", + "licenseMessageConfirm": "Pro potvrzení zadejte licenční klíč níže.", + "licenseQuestionRemove": "Jste si jisti, že chcete odstranit licenční klíč {selectedKey}?", + "licenseKeyDelete": "Odstranit licenční klíč", + "licenseKeyDeleteConfirm": "Potvrdit odstranění licenčního klíče", + "licenseTitle": "Správa stavu licence", + "licenseTitleDescription": "Zobrazit a spravovat licenční klíče v systému", + "licenseHost": "Licence hostitele", + "licenseHostDescription": "Správa hlavního licenčního klíče pro hostitele.", + "licensedNot": "Bez licence", + "hostId": "ID hostitele", + "licenseReckeckAll": "Znovu zobrazit všechny klíče", + "licenseSiteUsage": "Využití stránek", + "licenseSiteUsageDecsription": "Zobrazit počet stránek používajících tuto licenci.", + "licenseNoSiteLimit": "Neexistuje žádný limit počtu webů používajících nelicencovaný hostitele.", + "licensePurchase": "Zakoupit licenci", + "licensePurchaseSites": "Zakoupit další stránky", + "licenseSitesUsedMax": "{usedSites} použitých stránek {maxSites}", + "licenseSitesUsed": "{count, plural, =0 {# stránek} one {# stránky} other {# stránek}}", + "licensePurchaseDescription": "Vyberte kolik stránek chcete {selectedMode, select, license {Zakupte si licenci. Vždy můžete přidat více webů později.} other {Přidejte k vaší existující licenci.}}", + "licenseFee": "Licenční poplatek", + "licensePriceSite": "Cena za stránku", + "total": "Celkem", + "licenseContinuePayment": "Pokračovat v platbě", + "pricingPage": "cenová stránka", + "pricingPortal": "Zobrazit nákupní portál", + "licensePricingPage": "Pro nejaktuálnější ceny a slevy navštivte ", + "invite": "Pozvánky", + "inviteRegenerate": "Obnovit pozvánku", + "inviteRegenerateDescription": "Zrušit předchozí pozvání a vytvořit nové", + "inviteRemove": "Odstranit pozvánku", + "inviteRemoveError": "Nepodařilo se odstranit pozvánku", + "inviteRemoveErrorDescription": "Došlo k chybě při odstraňování pozvánky.", + "inviteRemoved": "Pozvánka odstraněna", + "inviteRemovedDescription": "Pozvánka pro {email} byla odstraněna.", + "inviteQuestionRemove": "Jste si jisti, že chcete odstranit pozvánku {email}?", + "inviteMessageRemove": "Po odstranění, tato pozvánka již nebude platná. Později můžete uživatele znovu pozvat.", + "inviteMessageConfirm": "Pro potvrzení zadejte prosím níže uvedenou e-mailovou adresu.", + "inviteQuestionRegenerate": "Jste si jisti, že chcete obnovit pozvánku pro {email}? Tato akce zruší předchozí pozvánku.", + "inviteRemoveConfirm": "Potvrdit odstranění pozvánky", + "inviteRegenerated": "Pozvánka obnovena", + "inviteSent": "Nová pozvánka byla odeslána na {email}.", + "inviteSentEmail": "Poslat uživateli oznámení e-mailem", + "inviteGenerate": "Nová pozvánka byla vygenerována pro {email}.", "inviteDuplicateError": "Duplicate Invite", - "inviteDuplicateErrorDescription": "An invitation for this user already exists.", - "inviteRateLimitError": "Rate Limit Exceeded", - "inviteRateLimitErrorDescription": "You have exceeded the limit of 3 regenerations per hour. Please try again later.", - "inviteRegenerateError": "Failed to Regenerate Invitation", - "inviteRegenerateErrorDescription": "An error occurred while regenerating the invitation.", - "inviteValidityPeriod": "Validity Period", - "inviteValidityPeriodSelect": "Select validity period", - "inviteRegenerateMessage": "The invitation has been regenerated. The user must access the link below to accept the invitation.", - "inviteRegenerateButton": "Regenerate", - "expiresAt": "Expires At", - "accessRoleUnknown": "Unknown Role", - "placeholder": "Placeholder", - "userErrorOrgRemove": "Failed to remove user", - "userErrorOrgRemoveDescription": "An error occurred while removing the user.", - "userOrgRemoved": "User removed", - "userOrgRemovedDescription": "The user {email} has been removed from the organization.", - "userQuestionOrgRemove": "Are you sure you want to remove {email} from the organization?", - "userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.", - "userMessageOrgConfirm": "To confirm, please type the name of the of the user below.", - "userRemoveOrgConfirm": "Confirm Remove User", - "userRemoveOrg": "Remove User from Organization", - "users": "Users", - "accessRoleMember": "Member", - "accessRoleOwner": "Owner", - "userConfirmed": "Confirmed", - "idpNameInternal": "Internal", - "emailInvalid": "Invalid email address", - "inviteValidityDuration": "Please select a duration", - "accessRoleSelectPlease": "Please select a role", - "usernameRequired": "Username is required", - "idpSelectPlease": "Please select an identity provider", + "inviteDuplicateErrorDescription": "Pozvánka pro tohoto uživatele již existuje.", + "inviteRateLimitError": "Limit sazby překročen", + "inviteRateLimitErrorDescription": "Překročil jsi limit 3 regenerací za hodinu. Opakujte akci později.", + "inviteRegenerateError": "Nepodařilo se obnovit pozvánku", + "inviteRegenerateErrorDescription": "Došlo k chybě při obnovování pozvánky.", + "inviteValidityPeriod": "Doba platnosti", + "inviteValidityPeriodSelect": "Vyberte dobu platnosti", + "inviteRegenerateMessage": "Pozvánka byla obnovena. Uživatel musí mít přístup k níže uvedenému odkazu, aby mohl pozvánku přijmout.", + "inviteRegenerateButton": "Regenerovat", + "expiresAt": "Vyprší v", + "accessRoleUnknown": "Neznámá role", + "placeholder": "Zástupný symbol", + "userErrorOrgRemove": "Odstranění uživatele se nezdařilo", + "userErrorOrgRemoveDescription": "Došlo k chybě při odebírání uživatele.", + "userOrgRemoved": "Uživatel odstraněn", + "userOrgRemovedDescription": "Uživatel {email} byl odebrán z organizace.", + "userQuestionOrgRemove": "Jste si jisti, že chcete odstranit {email} z organizace?", + "userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.", + "userMessageOrgConfirm": "Pro potvrzení, zadejte prosím jméno uživatele níže.", + "userRemoveOrgConfirm": "Potvrdit odebrání uživatele", + "userRemoveOrg": "Odebrat uživatele z organizace", + "users": "Uživatelé", + "accessRoleMember": "Člen", + "accessRoleOwner": "Vlastník", + "userConfirmed": "Potvrzeno", + "idpNameInternal": "Interní", + "emailInvalid": "Neplatná e-mailová adresa", + "inviteValidityDuration": "Zvolte prosím dobu trvání", + "accessRoleSelectPlease": "Vyberte prosím roli", + "usernameRequired": "Uživatelské jméno je povinné", + "idpSelectPlease": "Vyberte poskytovatele identity", "idpGenericOidc": "Generic OAuth2/OIDC provider.", - "accessRoleErrorFetch": "Failed to fetch roles", - "accessRoleErrorFetchDescription": "An error occurred while fetching the roles", - "idpErrorFetch": "Failed to fetch identity providers", - "idpErrorFetchDescription": "An error occurred while fetching identity providers", - "userErrorExists": "User Already Exists", - "userErrorExistsDescription": "This user is already a member of the organization.", - "inviteError": "Failed to invite user", - "inviteErrorDescription": "An error occurred while inviting the user", - "userInvited": "User invited", - "userInvitedDescription": "The user has been successfully invited.", - "userErrorCreate": "Failed to create user", - "userErrorCreateDescription": "An error occurred while creating the user", - "userCreated": "User created", - "userCreatedDescription": "The user has been successfully created.", - "userTypeInternal": "Internal User", - "userTypeInternalDescription": "Invite a user to join your organization directly.", - "userTypeExternal": "External User", - "userTypeExternalDescription": "Create a user with an external identity provider.", - "accessUserCreateDescription": "Follow the steps below to create a new user", - "userSeeAll": "See All Users", - "userTypeTitle": "User Type", - "userTypeDescription": "Determine how you want to create the user", - "userSettings": "User Information", - "userSettingsDescription": "Enter the details for the new user", - "inviteEmailSent": "Send invite email to user", - "inviteValid": "Valid For", - "selectDuration": "Select duration", - "accessRoleSelect": "Select role", - "inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.", - "inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.", - "inviteExpiresIn": "The invite will expire in {days, plural, one {# day} other {# days}}.", - "idpTitle": "Identity Provider", - "idpSelect": "Select the identity provider for the external user", - "idpNotConfigured": "No identity providers are configured. Please configure an identity provider before creating external users.", - "usernameUniq": "This must match the unique username that exists in the selected identity provider.", - "emailOptional": "Email (Optional)", - "nameOptional": "Name (Optional)", - "accessControls": "Access Controls", - "userDescription2": "Manage the settings on this user", - "accessRoleErrorAdd": "Failed to add user to role", - "accessRoleErrorAddDescription": "An error occurred while adding user to the role.", - "userSaved": "User saved", - "userSavedDescription": "The user has been updated.", - "autoProvisioned": "Auto Provisioned", - "autoProvisionedDescription": "Allow this user to be automatically managed by identity provider", - "accessControlsDescription": "Manage what this user can access and do in the organization", - "accessControlsSubmit": "Save Access Controls", - "roles": "Roles", - "accessUsersRoles": "Manage Users & Roles", - "accessUsersRolesDescription": "Invite users and add them to roles to manage access to your organization", - "key": "Key", - "createdAt": "Created At", - "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", - "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - "proxyEnableSSL": "Enable SSL (https)", - "targetErrorFetch": "Failed to fetch targets", - "targetErrorFetchDescription": "An error occurred while fetching targets", - "siteErrorFetch": "Failed to fetch resource", - "siteErrorFetchDescription": "An error occurred while fetching resource", + "accessRoleErrorFetch": "Nepodařilo se načíst role", + "accessRoleErrorFetchDescription": "Při načítání rolí došlo k chybě", + "idpErrorFetch": "Nepodařilo se načíst poskytovatele identity", + "idpErrorFetchDescription": "Při načítání poskytovatelů identity došlo k chybě", + "userErrorExists": "Uživatel již existuje", + "userErrorExistsDescription": "Tento uživatel je již členem organizace.", + "inviteError": "Nepodařilo se pozvat uživatele", + "inviteErrorDescription": "Při pozvání uživatele došlo k chybě", + "userInvited": "Uživatel pozván", + "userInvitedDescription": "Uživatel byl úspěšně pozván.", + "userErrorCreate": "Nepodařilo se vytvořit uživatele", + "userErrorCreateDescription": "Došlo k chybě při vytváření uživatele", + "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.", + "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", + "userSeeAll": "Zobrazit všechny uživatele", + "userTypeTitle": "Typ uživatele", + "userTypeDescription": "Určete, jak chcete vytvořit uživatele", + "userSettings": "Informace o uživateli", + "userSettingsDescription": "Zadejte podrobnosti pro nového uživatele", + "inviteEmailSent": "Poslat uživateli pozvánku", + "inviteValid": "Platné pro", + "selectDuration": "Vyberte dobu trvání", + "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.", + "inviteExpiresIn": "Pozvánka vyprší za {days, plural, one {# den} other {# days}}.", + "idpTitle": "Poskytovatel identity", + "idpSelect": "Vyberte poskytovatele identity pro externího uživatele", + "idpNotConfigured": "Nejsou nakonfigurováni žádní poskytovatelé identity. Před vytvořením externích uživatelů prosím nakonfigurujte poskytovatele identity.", + "usernameUniq": "Toto musí odpovídat jedinečné uživatelské jméno, které existuje ve vybraném poskytovateli identity.", + "emailOptional": "E-mail (nepovinné)", + "nameOptional": "Jméno (nepovinné)", + "accessControls": "Kontrola přístupu", + "userDescription2": "Spravovat nastavení tohoto uživatele", + "accessRoleErrorAdd": "Přidání uživatele do role se nezdařilo", + "accessRoleErrorAddDescription": "Došlo k chybě při přidávání uživatele do role.", + "userSaved": "Uživatel uložen", + "userSavedDescription": "Uživatel byl aktualizován.", + "autoProvisioned": "Automaticky poskytnuto", + "autoProvisionedDescription": "Povolit tomuto uživateli automaticky spravovat poskytovatel identity", + "accessControlsDescription": "Spravovat co může tento uživatel přistupovat a dělat v organizaci", + "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", + "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.", + "target": "Target", + "configureTarget": "Konfigurace cílů", + "targetErrorFetch": "Nepodařilo se načíst cíle", + "targetErrorFetchDescription": "Při načítání cílů došlo k chybě", + "siteErrorFetch": "Nepodařilo se načíst zdroj", + "siteErrorFetchDescription": "Při načítání zdroje došlo k chybě", "targetErrorDuplicate": "Duplicate target", - "targetErrorDuplicateDescription": "A target with these settings already exists", + "targetErrorDuplicateDescription": "Cíl s těmito nastaveními již existuje", "targetWireGuardErrorInvalidIp": "Invalid target IP", - "targetWireGuardErrorInvalidIpDescription": "Target IP must be within the site subnet", - "targetsUpdated": "Targets updated", - "targetsUpdatedDescription": "Targets and settings updated successfully", - "targetsErrorUpdate": "Failed to update targets", - "targetsErrorUpdateDescription": "An error occurred while updating targets", - "targetTlsUpdate": "TLS settings updated", - "targetTlsUpdateDescription": "Your TLS settings have been updated successfully", - "targetErrorTlsUpdate": "Failed to update TLS settings", - "targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings", - "proxyUpdated": "Proxy settings updated", - "proxyUpdatedDescription": "Your proxy settings have been updated successfully", - "proxyErrorUpdate": "Failed to update proxy settings", - "proxyErrorUpdateDescription": "An error occurred while updating proxy settings", + "targetWireGuardErrorInvalidIpDescription": "Cílová IP adresa musí být v podsíti webu", + "targetsUpdated": "Cíle byly aktualizovány", + "targetsUpdatedDescription": "Cíle a nastavení byly úspěšně aktualizovány", + "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", + "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", + "proxyErrorUpdate": "Aktualizace nastavení proxy se nezdařila", + "proxyErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení proxy", "targetAddr": "IP / Hostname", - "targetPort": "Port", - "targetProtocol": "Protocol", - "targetTlsSettings": "Secure Connection Configuration", - "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", - "targetTlsSettingsAdvanced": "Advanced TLS Settings", - "targetTlsSni": "TLS Server Name (SNI)", - "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", - "targetTlsSubmit": "Save Settings", - "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your backend services", - "targetStickySessions": "Enable Sticky Sessions", - "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", - "methodSelect": "Select method", + "targetPort": "Přístav", + "targetProtocol": "Protokol", + "targetTlsSettings": "Nastavení bezpečného připojení", + "targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument", + "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", + "targetStickySessions": "Povolit Rychlé relace", + "targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.", + "methodSelect": "Vyberte metodu", "targetSubmit": "Add Target", - "targetNoOne": "No targets. Add a target using the form.", - "targetNoOneDescription": "Adding more than one target above will enable load balancing.", - "targetsSubmit": "Save Targets", - "proxyAdditional": "Additional Proxy Settings", - "proxyAdditionalDescription": "Configure how your resource handles proxy settings", - "proxyCustomHeader": "Custom Host Header", - "proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.", - "proxyAdditionalSubmit": "Save Proxy Settings", - "subnetMaskErrorInvalid": "Invalid subnet mask. Must be between 0 and 32.", - "ipAddressErrorInvalidFormat": "Invalid IP address format", - "ipAddressErrorInvalidOctet": "Invalid IP address octet", - "path": "Path", - "matchPath": "Match Path", - "ipAddressRange": "IP Range", - "rulesErrorFetch": "Failed to fetch rules", - "rulesErrorFetchDescription": "An error occurred while fetching rules", - "rulesErrorDuplicate": "Duplicate rule", - "rulesErrorDuplicateDescription": "A rule with these settings already exists", - "rulesErrorInvalidIpAddressRange": "Invalid CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Please enter a valid CIDR value", - "rulesErrorInvalidUrl": "Invalid URL path", - "rulesErrorInvalidUrlDescription": "Please enter a valid URL path value", - "rulesErrorInvalidIpAddress": "Invalid IP", - "rulesErrorInvalidIpAddressDescription": "Please enter a valid IP address", - "rulesErrorUpdate": "Failed to update rules", - "rulesErrorUpdateDescription": "An error occurred while updating rules", - "rulesUpdated": "Enable Rules", - "rulesUpdatedDescription": "Rule evaluation has been updated", - "rulesMatchIpAddressRangeDescription": "Enter an address in CIDR format (e.g., 103.21.244.0/22)", - "rulesMatchIpAddress": "Enter an IP address (e.g., 103.21.244.12)", - "rulesMatchUrl": "Enter a URL path or pattern (e.g., /api/v1/todos or /api/v1/*)", - "rulesErrorInvalidPriority": "Invalid Priority", - "rulesErrorInvalidPriorityDescription": "Please enter a valid priority", - "rulesErrorDuplicatePriority": "Duplicate Priorities", - "rulesErrorDuplicatePriorityDescription": "Please enter unique priorities", - "ruleUpdated": "Rules updated", - "ruleUpdatedDescription": "Rules updated successfully", - "ruleErrorUpdate": "Operation failed", - "ruleErrorUpdateDescription": "An error occurred during the save operation", - "rulesPriority": "Priority", - "rulesAction": "Action", - "rulesMatchType": "Match Type", - "value": "Value", - "rulesAbout": "About Rules", - "rulesAboutDescription": "Rules allow you to control access to your resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.", - "rulesActions": "Actions", - "rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods", - "rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted", - "rulesActionPassToAuth": "Pass to Auth: Allow authentication methods to be attempted", - "rulesMatchCriteria": "Matching Criteria", - "rulesMatchCriteriaIpAddress": "Match a specific IP address", - "rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation", - "rulesMatchCriteriaUrl": "Match a URL path or pattern", - "rulesEnable": "Enable Rules", - "rulesEnableDescription": "Enable or disable rule evaluation for this resource", - "rulesResource": "Resource Rules Configuration", - "rulesResourceDescription": "Configure rules to control access to your resource", - "ruleSubmit": "Add Rule", - "rulesNoOne": "No rules. Add a rule using the form.", - "rulesOrder": "Rules are evaluated by priority in ascending order.", - "rulesSubmit": "Save Rules", - "resourceErrorCreate": "Error creating resource", - "resourceErrorCreateDescription": "An error occurred when creating the resource", - "resourceErrorCreateMessage": "Error creating resource:", - "resourceErrorCreateMessageDescription": "An unexpected error occurred", - "sitesErrorFetch": "Error fetching sites", - "sitesErrorFetchDescription": "An error occurred when fetching the sites", - "domainsErrorFetch": "Error fetching domains", - "domainsErrorFetchDescription": "An error occurred when fetching the domains", - "none": "None", - "unknown": "Unknown", - "resources": "Resources", - "resourcesDescription": "Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.", - "resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption", - "resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods", - "resourcesUsersRolesAccess": "User and role-based access control", - "resourcesErrorUpdate": "Failed to toggle resource", - "resourcesErrorUpdateDescription": "An error occurred while updating the resource", - "access": "Access", - "shareLink": "{resource} Share Link", - "resourceSelect": "Select resource", - "shareLinks": "Share Links", - "share": "Shareable Links", - "shareDescription2": "Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.", - "shareEasyCreate": "Easy to create and share", - "shareConfigurableExpirationDuration": "Configurable expiration duration", - "shareSecureAndRevocable": "Secure and revocable", - "nameMin": "Name must be at least {len} characters.", - "nameMax": "Name must not be longer than {len} characters.", - "sitesConfirmCopy": "Please confirm that you have copied the config.", - "unknownCommand": "Unknown command", - "newtErrorFetchReleases": "Failed to fetch release info: {err}", - "newtErrorFetchLatest": "Error fetching latest release: {err}", + "targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na vaši 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", + "targetErrorInvalidIp": "Neplatná IP adresa", + "targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele", + "targetErrorInvalidPort": "Neplatný port", + "targetErrorInvalidPortDescription": "Zadejte platné číslo portu", + "targetErrorNoSite": "Není vybrán žádný web", + "targetErrorNoSiteDescription": "Vyberte prosím web pro cíl", + "targetCreated": "Cíl byl vytvořen", + "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", + "save": "Uložit", + "proxyAdditional": "Další nastavení proxy", + "proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje", + "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", + "subnetMaskErrorInvalid": "Neplatná maska subsítě. Musí být mezi 0 a 32.", + "ipAddressErrorInvalidFormat": "Neplatný formát IP adresy", + "ipAddressErrorInvalidOctet": "Neplatná IP adresa octet", + "path": "Cesta", + "matchPath": "Cesta k zápasu", + "ipAddressRange": "Rozsah IP", + "rulesErrorFetch": "Nepodařilo se načíst pravidla", + "rulesErrorFetchDescription": "Při načítání pravidel došlo k chybě", + "rulesErrorDuplicate": "Duplikovat pravidlo", + "rulesErrorDuplicateDescription": "Pravidlo s těmito nastaveními již existuje", + "rulesErrorInvalidIpAddressRange": "Neplatný CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "Zadejte prosím platnou hodnotu CIDR", + "rulesErrorInvalidUrl": "Neplatná URL cesta", + "rulesErrorInvalidUrlDescription": "Zadejte platnou hodnotu URL cesty", + "rulesErrorInvalidIpAddress": "Neplatná IP adresa", + "rulesErrorInvalidIpAddressDescription": "Zadejte prosím platnou IP adresu", + "rulesErrorUpdate": "Aktualizace pravidel se nezdařila", + "rulesErrorUpdateDescription": "Při aktualizaci pravidel došlo k chybě", + "rulesUpdated": "Povolit pravidla", + "rulesUpdatedDescription": "Hodnocení pravidel bylo aktualizováno", + "rulesMatchIpAddressRangeDescription": "Zadejte adresu ve formátu CIDR (např. 103.21.244.0/22)", + "rulesMatchIpAddress": "Zadejte IP adresu (např. 103.21.244.12)", + "rulesMatchUrl": "Zadejte URL cestu nebo vzor (např. /api/v1/todos nebo /api/v1/*)", + "rulesErrorInvalidPriority": "Neplatná Priorita", + "rulesErrorInvalidPriorityDescription": "Zadejte prosím platnou prioritu", + "rulesErrorDuplicatePriority": "Duplikovat priority", + "rulesErrorDuplicatePriorityDescription": "Zadejte prosím unikátní priority", + "ruleUpdated": "Pravidla byla aktualizována", + "ruleUpdatedDescription": "Pravidla byla úspěšně aktualizována", + "ruleErrorUpdate": "Operace selhala", + "ruleErrorUpdateDescription": "Při ukládání došlo k chybě", + "rulesPriority": "Priorita", + "rulesAction": "Akce", + "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.", + "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í", + "rulesActionPassToAuth": "Pass to Auth: Povolit autentizační metody", + "rulesMatchCriteria": "Odpovídající kritéria", + "rulesMatchCriteriaIpAddress": "Porovnat konkrétní IP adresu", + "rulesMatchCriteriaIpAddressRange": "Odpovídá rozsahu IP adres v CIDR značení", + "rulesMatchCriteriaUrl": "Porovnejte URL cestu nebo gesto", + "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", + "ruleSubmit": "Přidat pravidlo", + "rulesNoOne": "Žádná pravidla. Přidejte pravidlo pomocí formuláře.", + "rulesOrder": "Pravidla jsou hodnocena podle priority vzestupně.", + "rulesSubmit": "Uložit pravidla", + "resourceErrorCreate": "Chyba při vytváření zdroje", + "resourceErrorCreateDescription": "Při vytváření zdroje došlo k chybě", + "resourceErrorCreateMessage": "Chyba při vytváření zdroje:", + "resourceErrorCreateMessageDescription": "Došlo k neočekávané chybě", + "sitesErrorFetch": "Chyba při načítání stránek", + "sitesErrorFetchDescription": "Při načítání stránek došlo k chybě", + "domainsErrorFetch": "Chyba při načítání domén", + "domainsErrorFetchDescription": "Při načítání domén došlo k chybě", + "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.", + "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í", + "resourcesErrorUpdate": "Nepodařilo se přepnout zdroj", + "resourcesErrorUpdateDescription": "Došlo k chybě při aktualizaci zdroje", + "access": "Přístup", + "shareLink": "{resource} Sdílet odkaz", + "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í.", + "shareEasyCreate": "Snadné vytváření a sdílení", + "shareConfigurableExpirationDuration": "Konfigurovatelná doba vypršení platnosti", + "shareSecureAndRevocable": "Bezpečné a odvolatelné", + "nameMin": "Jméno musí být alespoň {len} znaků.", + "nameMax": "Název nesmí být delší než {len} znaků.", + "sitesConfirmCopy": "Potvrďte, že jste zkopírovali konfiguraci.", + "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": "Newt Secret Key", - "architecture": "Architecture", - "sites": "Sites", - "siteWgAnyClients": "Use any WireGuard client to connect. You will have to address your internal resources using the peer IP.", - "siteWgCompatibleAllClients": "Compatible with all WireGuard clients", - "siteWgManualConfigurationRequired": "Manual configuration required", - "userErrorNotAdminOrOwner": "User is not an admin or owner", - "pangolinSettings": "Settings - Pangolin", - "accessRoleYour": "Your role:", - "accessRoleSelect2": "Select a role", - "accessUserSelect": "Select a user", - "otpEmailEnter": "Enter an email", - "otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.", - "otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.", - "otpEmailSmtpRequired": "SMTP Required", - "otpEmailSmtpRequiredDescription": "SMTP must be enabled on the server to use one-time password authentication.", - "otpEmailTitle": "One-time Passwords", - "otpEmailTitleDescription": "Require email-based authentication for resource access", - "otpEmailWhitelist": "Email Whitelist", - "otpEmailWhitelistList": "Whitelisted Emails", - "otpEmailWhitelistListDescription": "Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain.", - "otpEmailWhitelistSave": "Save Whitelist", - "passwordAdd": "Add Password", - "passwordRemove": "Remove Password", - "pincodeAdd": "Add PIN Code", - "pincodeRemove": "Remove PIN Code", - "resourceAuthMethods": "Authentication Methods", - "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", - "resourceAuthSettingsSave": "Saved successfully", - "resourceAuthSettingsSaveDescription": "Authentication settings have been saved", - "resourceErrorAuthFetch": "Failed to fetch data", - "resourceErrorAuthFetchDescription": "An error occurred while fetching the data", - "resourceErrorPasswordRemove": "Error removing resource password", - "resourceErrorPasswordRemoveDescription": "An error occurred while removing the resource password", - "resourceErrorPasswordSetup": "Error setting resource password", - "resourceErrorPasswordSetupDescription": "An error occurred while setting the resource password", - "resourceErrorPincodeRemove": "Error removing resource pincode", - "resourceErrorPincodeRemoveDescription": "An error occurred while removing the resource pincode", - "resourceErrorPincodeSetup": "Error setting resource PIN code", - "resourceErrorPincodeSetupDescription": "An error occurred while setting the resource PIN code", - "resourceErrorUsersRolesSave": "Failed to set roles", - "resourceErrorUsersRolesSaveDescription": "An error occurred while setting the roles", - "resourceErrorWhitelistSave": "Failed to save whitelist", - "resourceErrorWhitelistSaveDescription": "An error occurred while saving the whitelist", - "resourcePasswordSubmit": "Enable Password Protection", - "resourcePasswordProtection": "Password Protection {status}", - "resourcePasswordRemove": "Resource password removed", - "resourcePasswordRemoveDescription": "The resource password has been removed successfully", - "resourcePasswordSetup": "Resource password set", - "resourcePasswordSetupDescription": "The resource password has been set successfully", - "resourcePasswordSetupTitle": "Set Password", - "resourcePasswordSetupTitleDescription": "Set a password to protect this resource", - "resourcePincode": "PIN Code", - "resourcePincodeSubmit": "Enable PIN Code Protection", - "resourcePincodeProtection": "PIN Code Protection {status}", - "resourcePincodeRemove": "Resource pincode removed", - "resourcePincodeRemoveDescription": "The resource password has been removed successfully", - "resourcePincodeSetup": "Resource PIN code set", - "resourcePincodeSetupDescription": "The resource pincode has been set successfully", - "resourcePincodeSetupTitle": "Set Pincode", - "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", - "resourceRoleDescription": "Admins can always access this resource.", - "resourceUsersRoles": "Users & Roles", - "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", - "resourceUsersRolesSubmit": "Save Users & Roles", - "resourceWhitelistSave": "Saved successfully", - "resourceWhitelistSaveDescription": "Whitelist settings have been saved", - "ssoUse": "Use Platform SSO", - "ssoUseDescription": "Existing users will only have to log in once for all resources that have this enabled.", - "proxyErrorInvalidPort": "Invalid port number", - "subdomainErrorInvalid": "Invalid subdomain", - "domainErrorFetch": "Error fetching domains", - "domainErrorFetchDescription": "An error occurred when fetching the domains", - "resourceErrorUpdate": "Failed to update resource", - "resourceErrorUpdateDescription": "An error occurred while updating the resource", - "resourceUpdated": "Resource updated", - "resourceUpdatedDescription": "The resource has been updated successfully", - "resourceErrorTransfer": "Failed to transfer resource", - "resourceErrorTransferDescription": "An error occurred while transferring the resource", - "resourceTransferred": "Resource transferred", - "resourceTransferredDescription": "The resource has been transferred successfully", - "resourceErrorToggle": "Failed to toggle resource", - "resourceErrorToggleDescription": "An error occurred while updating the resource", - "resourceVisibilityTitle": "Visibility", - "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", - "resourceGeneral": "General Settings", - "resourceGeneralDescription": "Configure the general settings for this resource", - "resourceEnable": "Enable Resource", - "resourceTransfer": "Transfer Resource", - "resourceTransferDescription": "Transfer this resource to a different site", - "resourceTransferSubmit": "Transfer Resource", - "siteDestination": "Destination Site", - "searchSites": "Search sites", - "accessRoleCreate": "Create Role", - "accessRoleCreateDescription": "Create a new role to group users and manage their permissions.", - "accessRoleCreateSubmit": "Create Role", - "accessRoleCreated": "Role created", - "accessRoleCreatedDescription": "The role has been successfully created.", - "accessRoleErrorCreate": "Failed to create role", - "accessRoleErrorCreateDescription": "An error occurred while creating the role.", - "accessRoleErrorNewRequired": "New role is required", - "accessRoleErrorRemove": "Failed to remove role", - "accessRoleErrorRemoveDescription": "An error occurred while removing the role.", - "accessRoleName": "Role Name", - "accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.", - "accessRoleRemove": "Remove Role", - "accessRoleRemoveDescription": "Remove a role from the organization", - "accessRoleRemoveSubmit": "Remove Role", - "accessRoleRemoved": "Role removed", - "accessRoleRemovedDescription": "The role has been successfully removed.", - "accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.", - "manage": "Manage", - "sitesNotFound": "No sites found.", - "pangolinServerAdmin": "Server Admin - Pangolin", - "licenseTierProfessional": "Professional License", - "licenseTierEnterprise": "Enterprise License", - "licenseTierCommercial": "Commercial License", - "licensed": "Licensed", - "yes": "Yes", - "no": "No", - "sitesAdditional": "Additional Sites", - "licenseKeys": "License Keys", - "sitestCountDecrease": "Decrease site count", - "sitestCountIncrease": "Increase site count", - "idpManage": "Manage Identity Providers", - "idpManageDescription": "View and manage identity providers in the system", - "idpDeletedDescription": "Identity provider deleted successfully", + "newtSecretKey": "Tajný klíč novinky", + "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.", + "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", + "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.", + "otpEmailSmtpRequired": "Vyžadováno SMTP", + "otpEmailSmtpRequiredDescription": "Pro použití jednorázového ověření heslem musí být na serveru povolen SMTP.", + "otpEmailTitle": "Jednorázová hesla", + "otpEmailTitleDescription": "Vyžadovat ověření pomocí e-mailu pro přístup ke zdrojům", + "otpEmailWhitelist": "Whitelist e-mailu", + "otpEmailWhitelistList": "Povolené e-maily", + "otpEmailWhitelistListDescription": "K tomuto zdroji budou mít přístup pouze uživatelé s těmito e-mailovými adresami. Budou vyzváni k zadání jednorázového hesla, které bude odesláno na svůj e-mail. Wildcards (*@example.com) lze použít pro povolení jakékoli e-mailové adresy z domény.", + "otpEmailWhitelistSave": "Uložit seznam povolených", + "passwordAdd": "Přidat heslo", + "passwordRemove": "Odstranit heslo", + "pincodeAdd": "Přidat PIN kód", + "pincodeRemove": "Odstranit PIN kód", + "resourceAuthMethods": "Metody ověřování", + "resourceAuthMethodsDescriptions": "Povolit přístup ke zdroji pomocí dodatečných metod autorizace", + "resourceAuthSettingsSave": "Úspěšně uloženo", + "resourceAuthSettingsSaveDescription": "Nastavení ověřování bylo uloženo", + "resourceErrorAuthFetch": "Nepodařilo se načíst data", + "resourceErrorAuthFetchDescription": "Při načítání dat došlo k chybě", + "resourceErrorPasswordRemove": "Chyba při odstraňování hesla zdroje", + "resourceErrorPasswordRemoveDescription": "Došlo k chybě při odstraňování hesla", + "resourceErrorPasswordSetup": "Chyba při nastavování hesla", + "resourceErrorPasswordSetupDescription": "Při nastavování hesla došlo k chybě", + "resourceErrorPincodeRemove": "Chyba při odstraňování zdrojového kódu", + "resourceErrorPincodeRemoveDescription": "Došlo k chybě při odstraňování zdroje pincode", + "resourceErrorPincodeSetup": "Chyba při nastavení zdrojového PIN kódu", + "resourceErrorPincodeSetupDescription": "Došlo k chybě při nastavování zdrojového PIN kódu", + "resourceErrorUsersRolesSave": "Nepodařilo se nastavit role", + "resourceErrorUsersRolesSaveDescription": "Při nastavování rolí došlo k chybě", + "resourceErrorWhitelistSave": "Nepodařilo se uložit seznam povolených", + "resourceErrorWhitelistSaveDescription": "Došlo k chybě při ukládání seznamu povolených", + "resourcePasswordSubmit": "Povolit ochranu heslem", + "resourcePasswordProtection": "Ochrana hesla {status}", + "resourcePasswordRemove": "Heslo zdroje odstraněno", + "resourcePasswordRemoveDescription": "Heslo zdroje bylo úspěšně odstraněno", + "resourcePasswordSetup": "Heslo zdroje nastaveno", + "resourcePasswordSetupDescription": "Heslo zdroje bylo úspěšně nastaveno", + "resourcePasswordSetupTitle": "Nastavit heslo", + "resourcePasswordSetupTitleDescription": "Nastavte heslo pro ochranu tohoto zdroje", + "resourcePincode": "PIN kód", + "resourcePincodeSubmit": "Povolit ochranu PIN kódu", + "resourcePincodeProtection": "Ochrana kódu PIN {status}", + "resourcePincodeRemove": "Zdroj byl odstraněn", + "resourcePincodeRemoveDescription": "Heslo zdroje bylo úspěšně odstraněno", + "resourcePincodeSetup": "PIN kód zdroje nastaven", + "resourcePincodeSetupDescription": "Zdroj byl úspěšně nastaven", + "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", + "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", + "resourceWhitelistSaveDescription": "Nastavení seznamu povolených položek bylo uloženo", + "ssoUse": "Použít platformu SSO", + "ssoUseDescription": "Existující uživatelé se budou muset přihlásit pouze jednou pro všechny zdroje, které jsou povoleny.", + "proxyErrorInvalidPort": "Neplatné číslo portu", + "subdomainErrorInvalid": "Neplatná subdoména", + "domainErrorFetch": "Chyba při načítání domén", + "domainErrorFetchDescription": "Při načítání domén došlo k chybě", + "resourceErrorUpdate": "Aktualizace zdroje se nezdařila", + "resourceErrorUpdateDescription": "Došlo k chybě při aktualizaci zdroje", + "resourceUpdated": "Zdroj byl aktualizován", + "resourceUpdatedDescription": "Zdroj byl úspěšně aktualizován", + "resourceErrorTransfer": "Nepodařilo se přenést dokument", + "resourceErrorTransferDescription": "Došlo k chybě při přenosu zdroje", + "resourceTransferred": "Přenesené zdroje", + "resourceTransferredDescription": "Zdroj byl úspěšně přenesen", + "resourceErrorToggle": "Nepodařilo se přepnout zdroj", + "resourceErrorToggleDescription": "Došlo k chybě při aktualizaci zdroje", + "resourceVisibilityTitle": "Viditelnost", + "resourceVisibilityTitleDescription": "Zcela povolit nebo zakázat viditelnost zdrojů", + "resourceGeneral": "Obecná nastavení", + "resourceGeneralDescription": "Konfigurace obecných nastavení tohoto zdroje", + "resourceEnable": "Povolit dokument", + "resourceTransfer": "Přenos zdroje", + "resourceTransferDescription": "Přenést tento zdroj na jiný web", + "resourceTransferSubmit": "Přenos zdroje", + "siteDestination": "Cílová stránka", + "searchSites": "Hledat lokality", + "accessRoleCreate": "Vytvořit roli", + "accessRoleCreateDescription": "Vytvořte novou roli pro seskupení uživatelů a spravujte jejich oprávnění.", + "accessRoleCreateSubmit": "Vytvořit roli", + "accessRoleCreated": "Role vytvořena", + "accessRoleCreatedDescription": "Role byla úspěšně vytvořena.", + "accessRoleErrorCreate": "Nepodařilo se vytvořit roli", + "accessRoleErrorCreateDescription": "Došlo k chybě při vytváření role.", + "accessRoleErrorNewRequired": "Je vyžadována nová role", + "accessRoleErrorRemove": "Nepodařilo se odstranit roli", + "accessRoleErrorRemoveDescription": "Došlo k chybě při odstraňování role.", + "accessRoleName": "Název role", + "accessRoleQuestionRemove": "Chystáte se odstranit {name} roli. Tuto akci nelze vrátit zpět.", + "accessRoleRemove": "Odstranit roli", + "accessRoleRemoveDescription": "Odebrat roli z organizace", + "accessRoleRemoveSubmit": "Odstranit roli", + "accessRoleRemoved": "Role odstraněna", + "accessRoleRemovedDescription": "Role byla úspěšně odstraněna.", + "accessRoleRequiredRemove": "Před odstraněním této role vyberte novou roli, do které chcete převést existující členy.", + "manage": "Spravovat", + "sitesNotFound": "Nebyly nalezeny žádné stránky.", + "pangolinServerAdmin": "Správce serveru - Pangolin", + "licenseTierProfessional": "Profesionální licence", + "licenseTierEnterprise": "Podniková licence", + "licenseTierPersonal": "Personal License", + "licensed": "Licencováno", + "yes": "Ano", + "no": "Ne", + "sitesAdditional": "Další weby", + "licenseKeys": "Licenční klíče", + "sitestCountDecrease": "Snížit počet stránek", + "sitestCountIncrease": "Zvýšit počet stránek", + "idpManage": "Spravovat poskytovatele identity", + "idpManageDescription": "Zobrazit a spravovat poskytovatele identity v systému", + "idpDeletedDescription": "Poskytovatel identity byl úspěšně odstraněn", "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?", - "idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.", - "idpMessageConfirm": "To confirm, please type the name of the identity provider below.", - "idpConfirmDelete": "Confirm Delete Identity Provider", - "idpDelete": "Delete Identity Provider", - "idp": "Identity Providers", - "idpSearch": "Search identity providers...", - "idpAdd": "Add Identity Provider", - "idpClientIdRequired": "Client ID is required.", - "idpClientSecretRequired": "Client Secret is required.", - "idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.", - "idpErrorTokenUrlInvalid": "Token URL must be a valid URL.", - "idpPathRequired": "Identifier Path is required.", - "idpScopeRequired": "Scopes are required.", - "idpOidcDescription": "Configure an OpenID Connect identity provider", - "idpCreatedDescription": "Identity provider created successfully", - "idpCreate": "Create Identity Provider", - "idpCreateDescription": "Configure a new identity provider for user authentication", - "idpSeeAll": "See All Identity Providers", - "idpSettingsDescription": "Configure the basic information for your identity provider", - "idpDisplayName": "A display name for this identity provider", - "idpAutoProvisionUsers": "Auto Provision Users", - "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", - "licenseBadge": "Professional", - "idpType": "Provider Type", - "idpTypeDescription": "Select the type of identity provider you want to configure", - "idpOidcConfigure": "OAuth2/OIDC Configuration", - "idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials", - "idpClientId": "Client ID", - "idpClientIdDescription": "The OAuth2 client ID from your identity provider", - "idpClientSecret": "Client Secret", - "idpClientSecretDescription": "The OAuth2 client secret from your identity provider", - "idpAuthUrl": "Authorization URL", - "idpAuthUrlDescription": "The OAuth2 authorization endpoint URL", - "idpTokenUrl": "Token URL", - "idpTokenUrlDescription": "The OAuth2 token endpoint URL", - "idpOidcConfigureAlert": "Important Information", - "idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in your identity provider's settings. The callback URL will be provided after successful creation.", - "idpToken": "Token Configuration", - "idpTokenDescription": "Configure how to extract user information from the ID token", - "idpJmespathAbout": "About JMESPath", - "idpJmespathAboutDescription": "The paths below use JMESPath syntax to extract values from the ID token.", - "idpJmespathAboutDescriptionLink": "Learn more about JMESPath", - "idpJmespathLabel": "Identifier Path", - "idpJmespathLabelDescription": "The path to the user identifier in the ID token", - "idpJmespathEmailPathOptional": "Email Path (Optional)", - "idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token", - "idpJmespathNamePathOptional": "Name Path (Optional)", - "idpJmespathNamePathOptionalDescription": "The path to the user's name in the ID token", - "idpOidcConfigureScopes": "Scopes", - "idpOidcConfigureScopesDescription": "Space-separated list of OAuth2 scopes to request", - "idpSubmit": "Create Identity Provider", - "orgPolicies": "Organization Policies", - "idpSettings": "{idpName} Settings", - "idpCreateSettingsDescription": "Configure the settings for your identity provider", - "roleMapping": "Role Mapping", - "orgMapping": "Organization Mapping", - "orgPoliciesSearch": "Search organization policies...", - "orgPoliciesAdd": "Add Organization Policy", - "orgRequired": "Organization is required", - "error": "Error", - "success": "Success", - "orgPolicyAddedDescription": "Policy added successfully", - "orgPolicyUpdatedDescription": "Policy updated successfully", - "orgPolicyDeletedDescription": "Policy deleted successfully", - "defaultMappingsUpdatedDescription": "Default mappings updated successfully", - "orgPoliciesAbout": "About Organization Policies", - "orgPoliciesAboutDescription": "Organization policies are used to control access to organizations based on the user's ID token. You can specify JMESPath expressions to extract role and organization information from the ID token.", - "orgPoliciesAboutDescriptionLink": "See documentation, for more information.", - "defaultMappingsOptional": "Default Mappings (Optional)", - "defaultMappingsOptionalDescription": "The default mappings are used when when there is not an organization policy defined for an organization. You can specify the default role and organization mappings to fall back to here.", - "defaultMappingsRole": "Default Role Mapping", - "defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.", - "defaultMappingsOrg": "Default Organization Mapping", - "defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.", - "defaultMappingsSubmit": "Save Default Mappings", - "orgPoliciesEdit": "Edit Organization Policy", - "org": "Organization", - "orgSelect": "Select organization", - "orgSearch": "Search org", - "orgNotFound": "No org found.", - "roleMappingPathOptional": "Role Mapping Path (Optional)", - "orgMappingPathOptional": "Organization Mapping Path (Optional)", - "orgPolicyUpdate": "Update Policy", - "orgPolicyAdd": "Add Policy", - "orgPolicyConfig": "Configure access for an organization", - "idpUpdatedDescription": "Identity provider updated successfully", - "redirectUrl": "Redirect URL", - "redirectUrlAbout": "About Redirect URL", - "redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in your identity provider settings.", + "idpQuestionRemove": "Jste si jisti, že chcete trvale odstranit poskytovatele identity {name}?", + "idpMessageRemove": "Tímto odstraníte poskytovatele identity a všechny přidružené konfigurace. Uživatelé, kteří se autentizují prostřednictvím tohoto poskytovatele, se již nebudou moci přihlásit.", + "idpMessageConfirm": "Pro potvrzení zadejte níže uvedené jméno poskytovatele identity.", + "idpConfirmDelete": "Potvrdit odstranění poskytovatele identity", + "idpDelete": "Odstranit poskytovatele identity", + "idp": "Poskytovatelé identity", + "idpSearch": "Hledat poskytovatele identity...", + "idpAdd": "Přidat poskytovatele identity", + "idpClientIdRequired": "Je vyžadováno ID klienta.", + "idpClientSecretRequired": "Tajný klíč klienta je povinný.", + "idpErrorAuthUrlInvalid": "Ověřovací URL musí být platná adresa URL.", + "idpErrorTokenUrlInvalid": "URL tokenu musí být platná adresa URL.", + "idpPathRequired": "Je vyžadována cesta identifikátora.", + "idpScopeRequired": "Rozsahy jsou povinné.", + "idpOidcDescription": "Konfigurace OpenID Connect identitu poskytovatele", + "idpCreatedDescription": "Poskytovatel identity byl úspěšně vytvořen", + "idpCreate": "Vytvořit poskytovatele identity", + "idpCreateDescription": "Konfigurace nového poskytovatele identity pro ověření uživatele", + "idpSeeAll": "Zobrazit všechny poskytovatele identity", + "idpSettingsDescription": "Konfigurace základních informací pro svého poskytovatele identity", + "idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity", + "idpAutoProvisionUsers": "Automatická úprava uživatelů", + "idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.", + "licenseBadge": "EE", + "idpType": "Typ poskytovatele", + "idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat", + "idpOidcConfigure": "Nastavení OAuth2/OIDC", + "idpOidcConfigureDescription": "Konfigurace koncových bodů a pověření poskytovatele OAuth2/OIDC", + "idpClientId": "ID klienta", + "idpClientIdDescription": "Klientské ID OAuth2 od poskytovatele identity", + "idpClientSecret": "Tajný klíč klienta", + "idpClientSecretDescription": "OAuth2 klíč klienta od poskytovatele identity", + "idpAuthUrl": "Autorizační URL", + "idpAuthUrlDescription": "Koncový koncový bod ověření OAuth2", + "idpTokenUrl": "URL tokenu", + "idpTokenUrlDescription": "URL koncového bodu OAuth2", + "idpOidcConfigureAlert": "Důležité informace", + "idpOidcConfigureAlertDescription": "Po vytvoření poskytovatele identity budete muset nakonfigurovat URL zpětného volání v nastavení poskytovatele identity. Adresa URL zpětného volání bude poskytnuta po úspěšném vytvoření.", + "idpToken": "Nastavení tokenu", + "idpTokenDescription": "Konfigurovat jak extrahovat informace o uživateli z ID tokenu", + "idpJmespathAbout": "O JMESPath", + "idpJmespathAboutDescription": "Cesty níže používají JMESPath syntax pro extrakci hodnot z ID tokenu.", + "idpJmespathAboutDescriptionLink": "Další informace o cestě JMESPath", + "idpJmespathLabel": "Cesta identifikátoru", + "idpJmespathLabelDescription": "Cesta k identifikátoru uživatele v tokenu ID", + "idpJmespathEmailPathOptional": "Cesta e-mailu (volitelné)", + "idpJmespathEmailPathOptionalDescription": "Cesta k e-mailu uživatele v ID tokenu", + "idpJmespathNamePathOptional": "Cesta k názvu (volitelné)", + "idpJmespathNamePathOptionalDescription": "Cesta ke jménu uživatele v identifikačním tokenu", + "idpOidcConfigureScopes": "Rozsah", + "idpOidcConfigureScopesDescription": "Seznam OAuth2 rozsahů oddělených mezerou", + "idpSubmit": "Vytvořit poskytovatele identity", + "orgPolicies": "Zásady organizace", + "idpSettings": "Nastavení {idpName}", + "idpCreateSettingsDescription": "Konfigurace nastavení pro poskytovatele identity", + "roleMapping": "Mapování rolí", + "orgMapping": "Mapování organizace", + "orgPoliciesSearch": "Hledat v zásadách organizace...", + "orgPoliciesAdd": "Přidat zásady organizace", + "orgRequired": "Organizace je povinná", + "error": "Chyba", + "success": "Úspěšně", + "orgPolicyAddedDescription": "Politika byla úspěšně přidána", + "orgPolicyUpdatedDescription": "Zásady byly úspěšně aktualizovány", + "orgPolicyDeletedDescription": "Zásady byly úspěšně odstraněny", + "defaultMappingsUpdatedDescription": "Výchozí mapování bylo úspěšně aktualizováno", + "orgPoliciesAbout": "O zásadách organizace", + "orgPoliciesAboutDescription": "Zásady organizace se používají pro kontrolu přístupu do organizací na základě identifikačního tokenu. Můžete zadat JMESPath výrazy pro extrahování rolí a informací o organizaci z ID tokenu.", + "orgPoliciesAboutDescriptionLink": "Další informace naleznete v dokumentaci.", + "defaultMappingsOptional": "Výchozí mapování (volitelné)", + "defaultMappingsOptionalDescription": "Výchozí mapování se používá, pokud nejsou pro organizaci definovány zásady organizace. Můžete zadat výchozí roli a mapování organizace pro návrat zde.", + "defaultMappingsRole": "Výchozí mapování rolí", + "defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.", + "defaultMappingsOrg": "Výchozí mapování organizace", + "defaultMappingsOrgDescription": "Tento výraz musí vrátit org ID nebo pravdu, aby měl uživatel přístup k organizaci.", + "defaultMappingsSubmit": "Uložit výchozí mapování", + "orgPoliciesEdit": "Upravit zásady organizace", + "org": "Organizace", + "orgSelect": "Vyberte organizaci", + "orgSearch": "Hledat v org", + "orgNotFound": "Nenalezeny žádné org.", + "roleMappingPathOptional": "Cesta k mapování rolí (volitelné)", + "orgMappingPathOptional": "Cesta k mapování organizace (volitelné)", + "orgPolicyUpdate": "Aktualizovat přístupové právo", + "orgPolicyAdd": "Přidat přístupové právo", + "orgPolicyConfig": "Konfigurace přístupu pro organizaci", + "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.", "pangolinAuth": "Auth - Pangolin", - "verificationCodeLengthRequirements": "Your verification code must be 8 characters.", - "errorOccurred": "An error occurred", - "emailErrorVerify": "Failed to verify email:", - "emailVerified": "Email successfully verified! Redirecting you...", - "verificationCodeErrorResend": "Failed to resend verification code:", - "verificationCodeResend": "Verification code resent", - "verificationCodeResendDescription": "We've resent a verification code to your email address. Please check your inbox.", - "emailVerify": "Verify Email", - "emailVerifyDescription": "Enter the verification code sent to your email address.", - "verificationCode": "Verification Code", - "verificationCodeEmailSent": "We sent a verification code to your email address.", - "submit": "Submit", - "emailVerifyResendProgress": "Resending...", - "emailVerifyResend": "Didn't receive a code? Click here to resend", - "passwordNotMatch": "Passwords do not match", - "signupError": "An error occurred while signing up", - "pangolinLogoAlt": "Pangolin Logo", - "inviteAlready": "Looks like you've been invited!", - "inviteAlreadyDescription": "To accept the invite, you must log in or create an account.", - "signupQuestion": "Already have an account?", - "login": "Log in", - "resourceNotFound": "Resource Not Found", - "resourceNotFoundDescription": "The resource you're trying to access does not exist.", - "pincodeRequirementsLength": "PIN must be exactly 6 digits", - "pincodeRequirementsChars": "PIN must only contain numbers", - "passwordRequirementsLength": "Password must be at least 1 character long", - "passwordRequirementsTitle": "Password requirements:", - "passwordRequirementLength": "At least 8 characters long", - "passwordRequirementUppercase": "At least one uppercase letter", - "passwordRequirementLowercase": "At least one lowercase letter", - "passwordRequirementNumber": "At least one number", - "passwordRequirementSpecial": "At least one special character", - "passwordRequirementsMet": "✓ Password meets all requirements", - "passwordStrength": "Password strength", - "passwordStrengthWeak": "Weak", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Strong", - "passwordRequirements": "Requirements:", - "passwordRequirementLengthText": "8+ characters", - "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", - "passwordRequirementLowercaseText": "Lowercase letter (a-z)", - "passwordRequirementNumberText": "Number (0-9)", + "verificationCodeLengthRequirements": "Váš ověřovací kód musí mít 8 znaků.", + "errorOccurred": "Došlo k chybě", + "emailErrorVerify": "Nepodařilo se ověřit e-mail:", + "emailVerified": "E-mail byl úspěšně ověřen! Přesměrování vás...", + "verificationCodeErrorResend": "Nepodařilo se znovu odeslat ověřovací kód:", + "verificationCodeResend": "Ověřovací kód znovu odeslán", + "verificationCodeResendDescription": "Znovu jsme odeslali ověřovací kód na vaši e-mailovou adresu. Zkontrolujte prosím svou doručenou poštu.", + "emailVerify": "Ověřit e-mail", + "emailVerifyDescription": "Zadejte ověřovací kód zaslaný na vaši e-mailovou adresu.", + "verificationCode": "Ověřovací kód", + "verificationCodeEmailSent": "Na vaši e-mailovou adresu jsme zaslali ověřovací kód.", + "submit": "Odeslat", + "emailVerifyResendProgress": "Znovu odesílání...", + "emailVerifyResend": "Neobdrželi jste kód? Klikněte zde pro opětovné odeslání", + "passwordNotMatch": "Hesla se neshodují", + "signupError": "Při registraci došlo k chybě", + "pangolinLogoAlt": "Logo Pangolin", + "inviteAlready": "Vypadá to, že jste byli pozváni!", + "inviteAlreadyDescription": "Chcete-li přijmout pozvánku, musíte se přihlásit nebo vytvořit účet.", + "signupQuestion": "Již máte účet?", + "login": "Přihlásit se", + "resourceNotFound": "Zdroj nebyl nalezen", + "resourceNotFoundDescription": "Dokument, ke kterému se snažíte přistupovat, neexistuje.", + "pincodeRequirementsLength": "PIN musí být přesně 6 číslic", + "pincodeRequirementsChars": "PIN musí obsahovat pouze čísla", + "passwordRequirementsLength": "Heslo musí mít alespoň 1 znak", + "passwordRequirementsTitle": "Požadavky na hesla:", + "passwordRequirementLength": "Nejméně 8 znaků dlouhé", + "passwordRequirementUppercase": "Alespoň jedno velké písmeno", + "passwordRequirementLowercase": "Alespoň jedno malé písmeno", + "passwordRequirementNumber": "Alespoň jedno číslo", + "passwordRequirementSpecial": "Alespoň jeden speciální znak", + "passwordRequirementsMet": "✓ Heslo splňuje všechny požadavky", + "passwordStrength": "Síla hesla", + "passwordStrengthWeak": "Slabé", + "passwordStrengthMedium": "Střední", + "passwordStrengthStrong": "Silný", + "passwordRequirements": "Požadavky:", + "passwordRequirementLengthText": "8+ znaků", + "passwordRequirementUppercaseText": "Velké písmeno (A-Z)", + "passwordRequirementLowercaseText": "Malé písmeno (a-z)", + "passwordRequirementNumberText": "Číslo (0-9)", "passwordRequirementSpecialText": "Special character (!@#$%...)", - "passwordsDoNotMatch": "Passwords do not match", - "otpEmailRequirementsLength": "OTP must be at least 1 character long", - "otpEmailSent": "OTP Sent", - "otpEmailSentDescription": "An OTP has been sent to your email", - "otpEmailErrorAuthenticate": "Failed to authenticate with email", - "pincodeErrorAuthenticate": "Failed to authenticate with pincode", - "passwordErrorAuthenticate": "Failed to authenticate with password", - "poweredBy": "Powered by", - "authenticationRequired": "Authentication Required", - "authenticationMethodChoose": "Choose your preferred method to access {name}", - "authenticationRequest": "You must authenticate to access {name}", - "user": "User", - "pincodeInput": "6-digit PIN Code", - "pincodeSubmit": "Log in with PIN", - "passwordSubmit": "Log In with Password", - "otpEmailDescription": "A one-time code will be sent to this email.", - "otpEmailSend": "Send One-time Code", - "otpEmail": "One-Time Password (OTP)", - "otpEmailSubmit": "Submit OTP", - "backToEmail": "Back to Email", - "noSupportKey": "Server is running without a supporter key. Consider supporting the project!", - "accessDenied": "Access Denied", - "accessDeniedDescription": "You're not allowed to access this resource. If this is a mistake, please contact the administrator.", - "accessTokenError": "Error checking access token", - "accessGranted": "Access Granted", - "accessUrlInvalid": "Access URL Invalid", - "accessGrantedDescription": "You have been granted access to this resource. Redirecting you...", - "accessUrlInvalidDescription": "This shared access URL is invalid. Please contact the resource owner for a new URL.", - "tokenInvalid": "Invalid token", - "pincodeInvalid": "Invalid code", - "passwordErrorRequestReset": "Failed to request reset:", - "passwordErrorReset": "Failed to reset password:", - "passwordResetSuccess": "Password reset successfully! Back to log in...", - "passwordReset": "Reset Password", - "passwordResetDescription": "Follow the steps to reset your password", - "passwordResetSent": "We'll send a password reset code to this email address.", + "passwordsDoNotMatch": "Hesla se neshodují", + "otpEmailRequirementsLength": "OTP musí mít alespoň 1 znak", + "otpEmailSent": "OTP odesláno", + "otpEmailSentDescription": "OTP bylo odesláno na váš e-mail", + "otpEmailErrorAuthenticate": "Ověření pomocí e-mailu se nezdařilo", + "pincodeErrorAuthenticate": "Ověření pomocí pincode se nezdařilo", + "passwordErrorAuthenticate": "Ověření pomocí hesla se nezdařilo", + "poweredBy": "Běží na", + "authenticationRequired": "Vyžadována autentizace", + "authenticationMethodChoose": "Vyberte si preferovanou metodu pro přístup k {name}", + "authenticationRequest": "Musíte se přihlásit k přístupu {name}", + "user": "Uživatel", + "pincodeInput": "6místný PIN kód", + "pincodeSubmit": "Přihlásit se pomocí PIN", + "passwordSubmit": "Přihlásit se pomocí hesla", + "otpEmailDescription": "Na tento e-mail bude zaslán jednorázový kód.", + "otpEmailSend": "Poslat jednorázový kód", + "otpEmail": "Jednorázové heslo (OTP)", + "otpEmailSubmit": "Odeslat OTP", + "backToEmail": "Zpět na e-mail", + "noSupportKey": "Server běží bez klíče podporovatele. Zvažte podporu projektu!", + "accessDenied": "Přístup odepřen", + "accessDeniedDescription": "Nemáte oprávnění k přístupu k tomuto dokumentu. Pokud se jedná o chybu, obraťte se na správce.", + "accessTokenError": "Chyba při kontrole přístupu token", + "accessGranted": "Přístup povolen", + "accessUrlInvalid": "Neplatná URL adresa přístupu", + "accessGrantedDescription": "Byl vám udělen přístup k tomuto zdroji. Přesměrování vás...", + "accessUrlInvalidDescription": "Tato URL adresa sdíleného přístupu je neplatná. Kontaktujte prosím vlastníka zdroje pro novou URL.", + "tokenInvalid": "Neplatný token", + "pincodeInvalid": "Neplatný kód", + "passwordErrorRequestReset": "Nepodařilo se požádat o reset:", + "passwordErrorReset": "Obnovení hesla selhalo:", + "passwordResetSuccess": "Obnovení hesla úspěšné! Zpět do přihlášení...", + "passwordReset": "Obnovit heslo", + "passwordResetDescription": "Postupujte podle kroků pro obnovení hesla", + "passwordResetSent": "Na tuto e-mailovou adresu zašleme kód pro obnovení hesla.", "passwordResetCode": "Reset Code", - "passwordResetCodeDescription": "Check your email for the reset code.", - "passwordNew": "New Password", - "passwordNewConfirm": "Confirm New Password", - "pincodeAuth": "Authenticator Code", - "pincodeSubmit2": "Submit Code", - "passwordResetSubmit": "Request Reset", - "passwordBack": "Back to Password", - "loginBack": "Go back to log in", - "signup": "Sign up", - "loginStart": "Log in to get started", - "idpOidcTokenValidating": "Validating OIDC token", - "idpOidcTokenResponse": "Validate OIDC token response", - "idpErrorOidcTokenValidating": "Error validating OIDC token", - "idpConnectingTo": "Connecting to {name}", - "idpConnectingToDescription": "Validating your identity", - "idpConnectingToProcess": "Connecting...", - "idpConnectingToFinished": "Connected", - "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", - "idpErrorNotFound": "IdP not found", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", - "inviteInvalid": "Invalid Invite", - "inviteInvalidDescription": "The invite link is invalid.", - "inviteErrorWrongUser": "Invite is not for this user", - "inviteErrorUserNotExists": "User does not exist. Please create an account first.", - "inviteErrorLoginRequired": "You must be logged in to accept an invite", - "inviteErrorExpired": "The invite may have expired", - "inviteErrorRevoked": "The invite might have been revoked", - "inviteErrorTypo": "There could be a typo in the invite link", + "passwordResetCodeDescription": "Zkontrolujte svůj e-mail pro kód pro obnovení.", + "passwordNew": "Nové heslo", + "passwordNewConfirm": "Potvrdit nové heslo", + "pincodeAuth": "Ověřovací kód", + "pincodeSubmit2": "Odeslat kód", + "passwordResetSubmit": "Žádost o obnovení", + "passwordBack": "Zpět na heslo", + "loginBack": "Přejít zpět na přihlášení", + "signup": "Zaregistrovat se", + "loginStart": "Přihlaste se a začněte", + "idpOidcTokenValidating": "Ověřování OIDC tokenu", + "idpOidcTokenResponse": "Potvrdit odpověď OIDC tokenu", + "idpErrorOidcTokenValidating": "Chyba při ověřování OIDC tokenu", + "idpConnectingTo": "Připojování k {name}", + "idpConnectingToDescription": "Ověření Vaší identity", + "idpConnectingToProcess": "Připojování...", + "idpConnectingToFinished": "Připojeno", + "idpErrorConnectingTo": "Při připojování k {name}došlo k chybě. Obraťte se na správce.", + "idpErrorNotFound": "IdP nenalezen", + "inviteInvalid": "Neplatná pozvánka", + "inviteInvalidDescription": "Odkaz pro pozvání je neplatný.", + "inviteErrorWrongUser": "Pozvat není pro tohoto uživatele", + "inviteErrorUserNotExists": "Uživatel neexistuje. Nejprve si vytvořte účet.", + "inviteErrorLoginRequired": "Musíte být přihlášeni, abyste mohli přijmout pozvánku", + "inviteErrorExpired": "Pozvánka možná vypršela", + "inviteErrorRevoked": "Pozvánka mohla být zrušena", + "inviteErrorTypo": "Na pozvánce může být typol", "pangolinSetup": "Setup - Pangolin", - "orgNameRequired": "Organization name is required", - "orgIdRequired": "Organization ID is required", - "orgErrorCreate": "An error occurred while creating org", - "pageNotFound": "Page Not Found", - "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", - "overview": "Overview", - "home": "Home", - "accessControl": "Access Control", - "settings": "Settings", - "usersAll": "All Users", - "license": "License", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "No results found.", + "orgNameRequired": "Je vyžadován název organizace", + "orgIdRequired": "Je vyžadováno ID organizace", + "orgErrorCreate": "Při vytváření org došlo k chybě", + "pageNotFound": "Stránka nenalezena", + "pageNotFoundDescription": "Jejda! Stránka, kterou hledáte, neexistuje.", + "overview": "Přehled", + "home": "Domů", + "accessControl": "Kontrola přístupu", + "settings": "Nastavení", + "usersAll": "Všichni uživatelé", + "license": "Licence", + "pangolinDashboard": "Nástěnka - Pangolin", + "noResults": "Nebyly nalezeny žádné výsledky.", "terabytes": "{count} TB", "gigabytes": "{count} GB", "megabytes": "{count} MB", - "tagsEntered": "Entered Tags", - "tagsEnteredDescription": "These are the tags you`ve entered.", - "tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options", - "tagsWarnInvalid": "Invalid tag as per validateTag", - "tagWarnTooShort": "Tag {tagText} is too short", - "tagWarnTooLong": "Tag {tagText} is too long", - "tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed", - "tagWarnDuplicate": "Duplicate tag {tagText} not added", - "supportKeyInvalid": "Invalid Key", - "supportKeyInvalidDescription": "Your supporter key is invalid.", + "tagsEntered": "Zadané štítky", + "tagsEnteredDescription": "Toto jsou značky, které jste zadali.", + "tagsWarnCannotBeLessThanZero": "maxTagy a minTagy nesmí být menší než 0", + "tagsWarnNotAllowedAutocompleteOptions": "Označení není povoleno podle možností automatického dokončování", + "tagsWarnInvalid": "Neplatný štítek pro ověření tagu", + "tagWarnTooShort": "Značka {tagText} je příliš krátká", + "tagWarnTooLong": "Značka {tagText} je příliš dlouhá", + "tagsWarnReachedMaxNumber": "Dosažen maximální povolený počet štítků", + "tagWarnDuplicate": "Duplicitní značka {tagText} nebyla přidána", + "supportKeyInvalid": "Neplatný klíč", + "supportKeyInvalidDescription": "Váš supporter klíč je neplatný.", "supportKeyValid": "Valid Key", - "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", - "supportKeyErrorValidationDescription": "Failed to validate supporter key.", - "supportKey": "Support Development and Adopt a Pangolin!", - "supportKeyDescription": "Purchase a supporter key to help us continue developing Pangolin for the community. Your contribution allows us to commit more time to maintain and add new features to the application for everyone. We will never use this to paywall features. This is separate from any Commercial Edition.", - "supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!", - "supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on", - "supportKeyPurchaseLink": "our website", - "supportKeyPurchase2": "and redeem it here.", - "supportKeyLearnMore": "Learn more.", - "supportKeyOptions": "Please select the option that best suits you.", - "supportKetOptionFull": "Full Supporter", - "forWholeServer": "For the whole server", - "lifetimePurchase": "Lifetime purchase", - "supporterStatus": "Supporter status", - "buy": "Buy", - "supportKeyOptionLimited": "Limited Supporter", - "forFiveUsers": "For 5 or less users", - "supportKeyRedeem": "Redeem Supporter Key", - "supportKeyHideSevenDays": "Hide for 7 days", - "supportKeyEnter": "Enter Supporter Key", - "supportKeyEnterDescription": "Meet your very own pet Pangolin!", + "supportKeyValidDescription": "Váš klíč podporovatele byl ověřen. Děkujeme za vaši podporu!", + "supportKeyErrorValidationDescription": "Nepodařilo se ověřit klíč podporovatele.", + "supportKey": "Podpořte vývoj a přijměte Pangolin!", + "supportKeyDescription": "Kupte klíč podporovatele, který nám pomůže pokračovat ve vývoji Pangolinu pro komunitu. Váš příspěvek nám umožňuje získat více času pro zachování a přidání nových funkcí do aplikace pro všechny. Nikdy to nepoužijeme pro funkce Paywall a to je oddělené od jakékoliv obchodní verze.", + "supportKeyPet": "Také se dostanete k adopci a setkáte se s vlastním mazlíčkem Pangolinem!", + "supportKeyPurchase": "Platby jsou zpracovávány přes GitHub. Poté můžete získat svůj klíč na", + "supportKeyPurchaseLink": "naše webové stránky", + "supportKeyPurchase2": "a znovu jej zde vyplatit.", + "supportKeyLearnMore": "Zjistěte více.", + "supportKeyOptions": "Vyberte si prosím možnost, která vám nejlépe vyhovuje.", + "supportKetOptionFull": "Plný podporovatel", + "forWholeServer": "Pro celý server", + "lifetimePurchase": "Doživotní nákup", + "supporterStatus": "Stav podporovatele", + "buy": "Koupit", + "supportKeyOptionLimited": "Omezený podporovatel", + "forFiveUsers": "Pro 5 nebo méně uživatelů", + "supportKeyRedeem": "Uplatnit klíč podpory", + "supportKeyHideSevenDays": "Skrýt na 7 dní", + "supportKeyEnter": "Zadejte klíč podpory", + "supportKeyEnterDescription": "Seznamte se s vlastním mazlíčkem Pangolin!", "githubUsername": "GitHub Username", - "supportKeyInput": "Supporter Key", - "supportKeyBuy": "Buy Supporter Key", - "logoutError": "Error logging out", - "signingAs": "Signed in as", - "serverAdmin": "Server Admin", - "managedSelfhosted": "Managed Self-Hosted", - "otpEnable": "Enable Two-factor", - "otpDisable": "Disable Two-factor", - "logout": "Log Out", - "licenseTierProfessionalRequired": "Professional Edition Required", - "licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.", - "actionGetOrg": "Get Organization", - "updateOrgUser": "Update Org User", - "createOrgUser": "Create Org User", - "actionUpdateOrg": "Update Organization", - "actionUpdateUser": "Update User", - "actionGetUser": "Get User", - "actionGetOrgUser": "Get Organization User", - "actionListOrgDomains": "List Organization Domains", - "actionCreateSite": "Create Site", - "actionDeleteSite": "Delete Site", - "actionGetSite": "Get Site", - "actionListSites": "List Sites", - "actionApplyBlueprint": "Apply Blueprint", - "setupToken": "Setup Token", - "setupTokenDescription": "Enter the setup token from the server console.", - "setupTokenRequired": "Setup token is required", - "actionUpdateSite": "Update Site", - "actionListSiteRoles": "List Allowed Site Roles", - "actionCreateResource": "Create Resource", - "actionDeleteResource": "Delete Resource", - "actionGetResource": "Get Resource", - "actionListResource": "List Resources", - "actionUpdateResource": "Update Resource", - "actionListResourceUsers": "List Resource Users", - "actionSetResourceUsers": "Set Resource Users", - "actionSetAllowedResourceRoles": "Set Allowed Resource Roles", - "actionListAllowedResourceRoles": "List Allowed Resource Roles", - "actionSetResourcePassword": "Set Resource Password", - "actionSetResourcePincode": "Set Resource Pincode", - "actionSetResourceEmailWhitelist": "Set Resource Email Whitelist", - "actionGetResourceEmailWhitelist": "Get Resource Email Whitelist", + "supportKeyInput": "Klíč pro podporu", + "supportKeyBuy": "Koupit klíč pro podporu", + "logoutError": "Chyba při odhlášení", + "signingAs": "Přihlášen jako", + "serverAdmin": "Správce serveru", + "managedSelfhosted": "Spravované vlastní hostování", + "otpEnable": "Povolit dvoufaktorové", + "otpDisable": "Zakázat dvoufaktorové", + "logout": "Odhlásit se", + "licenseTierProfessionalRequired": "Vyžadována profesionální edice", + "licenseTierProfessionalRequiredDescription": "Tato funkce je dostupná pouze v Professional Edition.", + "actionGetOrg": "Získat organizaci", + "updateOrgUser": "Aktualizovat Org uživatele", + "createOrgUser": "Vytvořit Org uživatele", + "actionUpdateOrg": "Aktualizovat organizaci", + "actionUpdateUser": "Aktualizovat uživatele", + "actionGetUser": "Získat uživatele", + "actionGetOrgUser": "Získat uživatele organizace", + "actionListOrgDomains": "Seznam domén organizace", + "actionCreateSite": "Vytvořit lokalitu", + "actionDeleteSite": "Odstranění lokality", + "actionGetSite": "Získat web", + "actionListSites": "Seznam stránek", + "actionApplyBlueprint": "Použít plán", + "setupToken": "Nastavit token", + "setupTokenDescription": "Zadejte nastavovací token z konzole serveru.", + "setupTokenRequired": "Je vyžadován token nastavení", + "actionUpdateSite": "Aktualizovat stránku", + "actionListSiteRoles": "Seznam povolených rolí webu", + "actionCreateResource": "Vytvořit zdroj", + "actionDeleteResource": "Odstranit dokument", + "actionGetResource": "Získat dokument", + "actionListResource": "Seznam zdrojů", + "actionUpdateResource": "Aktualizovat dokument", + "actionListResourceUsers": "Seznam uživatelů zdrojů", + "actionSetResourceUsers": "Nastavit uživatele zdrojů", + "actionSetAllowedResourceRoles": "Nastavit povolené role zdrojů", + "actionListAllowedResourceRoles": "Seznam povolených rolí zdrojů", + "actionSetResourcePassword": "Nastavit heslo zdroje", + "actionSetResourcePincode": "Nastavit zdrojový kód", + "actionSetResourceEmailWhitelist": "Nastavit seznam povolených dokumentů", + "actionGetResourceEmailWhitelist": "Získat seznam povolených dokumentů", "actionCreateTarget": "Create Target", - "actionDeleteTarget": "Delete Target", - "actionGetTarget": "Get Target", - "actionListTargets": "List Targets", + "actionDeleteTarget": "Odstranit cíl", + "actionGetTarget": "Získat cíl", + "actionListTargets": "Seznam cílů", "actionUpdateTarget": "Update Target", - "actionCreateRole": "Create Role", - "actionDeleteRole": "Delete Role", - "actionGetRole": "Get Role", - "actionListRole": "List Roles", - "actionUpdateRole": "Update Role", - "actionListAllowedRoleResources": "List Allowed Role Resources", - "actionInviteUser": "Invite User", - "actionRemoveUser": "Remove User", - "actionListUsers": "List Users", - "actionAddUserRole": "Add User Role", - "actionGenerateAccessToken": "Generate Access Token", - "actionDeleteAccessToken": "Delete Access Token", - "actionListAccessTokens": "List Access Tokens", - "actionCreateResourceRule": "Create Resource Rule", - "actionDeleteResourceRule": "Delete Resource Rule", - "actionListResourceRules": "List Resource Rules", - "actionUpdateResourceRule": "Update Resource Rule", - "actionListOrgs": "List Organizations", - "actionCheckOrgId": "Check ID", - "actionCreateOrg": "Create Organization", - "actionDeleteOrg": "Delete Organization", - "actionListApiKeys": "List API Keys", - "actionListApiKeyActions": "List API Key Actions", - "actionSetApiKeyActions": "Set API Key Allowed Actions", - "actionCreateApiKey": "Create API Key", - "actionDeleteApiKey": "Delete API Key", - "actionCreateIdp": "Create IDP", - "actionUpdateIdp": "Update IDP", - "actionDeleteIdp": "Delete IDP", - "actionListIdps": "List IDP", - "actionGetIdp": "Get IDP", - "actionCreateIdpOrg": "Create IDP Org Policy", - "actionDeleteIdpOrg": "Delete IDP Org Policy", - "actionListIdpOrgs": "List IDP Orgs", - "actionUpdateIdpOrg": "Update IDP Org", - "actionCreateClient": "Create Client", - "actionDeleteClient": "Delete Client", - "actionUpdateClient": "Update Client", - "actionListClients": "List Clients", - "actionGetClient": "Get Client", - "actionCreateSiteResource": "Create Site Resource", - "actionDeleteSiteResource": "Delete Site Resource", - "actionGetSiteResource": "Get Site Resource", - "actionListSiteResources": "List Site Resources", - "actionUpdateSiteResource": "Update Site Resource", - "actionListInvitations": "List Invitations", - "noneSelected": "None selected", - "orgNotFound2": "No organizations found.", - "searchProgress": "Search...", - "create": "Create", - "orgs": "Organizations", - "loginError": "An error occurred while logging in", - "passwordForgot": "Forgot your password?", - "otpAuth": "Two-Factor Authentication", - "otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.", - "otpAuthSubmit": "Submit Code", - "idpContinue": "Or continue with", - "otpAuthBack": "Back to Log In", + "actionCreateRole": "Vytvořit roli", + "actionDeleteRole": "Odstranit roli", + "actionGetRole": "Získat roli", + "actionListRole": "Seznam rolí", + "actionUpdateRole": "Aktualizovat roli", + "actionListAllowedRoleResources": "Seznam povolených zdrojů rolí", + "actionInviteUser": "Pozvat uživatele", + "actionRemoveUser": "Odstranit uživatele", + "actionListUsers": "Seznam uživatelů", + "actionAddUserRole": "Přidat uživatelskou roli", + "actionGenerateAccessToken": "Generovat přístupový token", + "actionDeleteAccessToken": "Odstranit přístupový token", + "actionListAccessTokens": "Seznam přístupových tokenů", + "actionCreateResourceRule": "Vytvořit pravidlo pro zdroj", + "actionDeleteResourceRule": "Odstranit pravidlo pro dokument", + "actionListResourceRules": "Seznam pravidel zdrojů", + "actionUpdateResourceRule": "Aktualizovat pravidlo pro dokument", + "actionListOrgs": "Seznam organizací", + "actionCheckOrgId": "ID kontroly", + "actionCreateOrg": "Vytvořit organizaci", + "actionDeleteOrg": "Odstranit organizaci", + "actionListApiKeys": "Seznam API klíčů", + "actionListApiKeyActions": "Seznam akcí API klíče", + "actionSetApiKeyActions": "Nastavit povolené akce API klíče", + "actionCreateApiKey": "Vytvořit klíč API", + "actionDeleteApiKey": "Odstranit klíč API", + "actionCreateIdp": "Vytvořit IDP", + "actionUpdateIdp": "Aktualizovat IDP", + "actionDeleteIdp": "Odstranit IDP", + "actionListIdps": "ID seznamu", + "actionGetIdp": "Získat IDP", + "actionCreateIdpOrg": "Vytvořit IDP Org Policy", + "actionDeleteIdpOrg": "Odstranit IDP Org Policy", + "actionListIdpOrgs": "Seznam IDP orgánů", + "actionUpdateIdpOrg": "Aktualizovat IDP Org", + "actionCreateClient": "Vytvořit klienta", + "actionDeleteClient": "Odstranit klienta", + "actionUpdateClient": "Aktualizovat klienta", + "actionListClients": "Seznam klientů", + "actionGetClient": "Získat klienta", + "actionCreateSiteResource": "Vytvořit zdroj webu", + "actionDeleteSiteResource": "Odstranit dokument webu", + "actionGetSiteResource": "Získat zdroj webu", + "actionListSiteResources": "Seznam zdrojů webu", + "actionUpdateSiteResource": "Aktualizovat dokument webu", + "actionListInvitations": "Seznam pozvánek", + "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ě", + "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ů.", + "otpAuthSubmit": "Odeslat kód", + "idpContinue": "Nebo pokračovat s", + "otpAuthBack": "Zpět na přihlášení", "navbar": "Navigation Menu", - "navbarDescription": "Main navigation menu for the application", - "navbarDocsLink": "Documentation", - "commercialEdition": "Commercial Edition", - "otpErrorEnable": "Unable to enable 2FA", - "otpErrorEnableDescription": "An error occurred while enabling 2FA", - "otpSetupCheckCode": "Please enter a 6-digit code", - "otpSetupCheckCodeRetry": "Invalid code. Please try again.", - "otpSetup": "Enable Two-factor Authentication", - "otpSetupDescription": "Secure your account with an extra layer of protection", - "otpSetupScanQr": "Scan this QR code with your authenticator app or enter the secret key manually:", - "otpSetupSecretCode": "Authenticator Code", - "otpSetupSuccess": "Two-Factor Authentication Enabled", - "otpSetupSuccessStoreBackupCodes": "Your account is now more secure. Don't forget to save your backup codes.", - "otpErrorDisable": "Unable to disable 2FA", - "otpErrorDisableDescription": "An error occurred while disabling 2FA", - "otpRemove": "Disable Two-factor Authentication", - "otpRemoveDescription": "Disable two-factor authentication for your account", - "otpRemoveSuccess": "Two-Factor Authentication Disabled", - "otpRemoveSuccessMessage": "Two-factor authentication has been disabled for your account. You can enable it again at any time.", - "otpRemoveSubmit": "Disable 2FA", - "paginator": "Page {current} of {last}", - "paginatorToFirst": "Go to first page", - "paginatorToPrevious": "Go to previous page", - "paginatorToNext": "Go to next page", - "paginatorToLast": "Go to last page", - "copyText": "Copy text", - "copyTextFailed": "Failed to copy text: ", - "copyTextClipboard": "Copy to clipboard", - "inviteErrorInvalidConfirmation": "Invalid confirmation", - "passwordRequired": "Password is required", - "allowAll": "Allow All", - "permissionsAllowAll": "Allow All Permissions", - "githubUsernameRequired": "GitHub username is required", - "supportKeyRequired": "Supporter key is required", - "passwordRequirementsChars": "Password must be at least 8 characters", - "language": "Language", - "verificationCodeRequired": "Code is required", - "userErrorNoUpdate": "No user to update", - "siteErrorNoUpdate": "No site to update", - "resourceErrorNoUpdate": "No resource to update", - "authErrorNoUpdate": "No auth info to update", - "orgErrorNoUpdate": "No org to update", - "orgErrorNoProvided": "No org provided", - "apiKeysErrorNoUpdate": "No API key to update", - "sidebarOverview": "Overview", - "sidebarHome": "Home", - "sidebarSites": "Sites", - "sidebarResources": "Resources", - "sidebarAccessControl": "Access Control", - "sidebarUsers": "Users", - "sidebarInvitations": "Invitations", - "sidebarRoles": "Roles", - "sidebarShareableLinks": "Shareable Links", - "sidebarApiKeys": "API Keys", - "sidebarSettings": "Settings", - "sidebarAllUsers": "All Users", - "sidebarIdentityProviders": "Identity Providers", - "sidebarLicense": "License", - "sidebarClients": "Clients (Beta)", - "sidebarDomains": "Domains", - "enableDockerSocket": "Enable Docker Blueprint", - "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", - "enableDockerSocketLink": "Learn More", - "viewDockerContainers": "View Docker Containers", - "containersIn": "Containers in {siteName}", - "selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.", - "containerName": "Name", - "containerImage": "Image", - "containerState": "State", - "containerNetworks": "Networks", + "navbarDescription": "Hlavní navigační menu aplikace", + "navbarDocsLink": "Dokumentace", + "otpErrorEnable": "2FA nelze povolit", + "otpErrorEnableDescription": "Došlo k chybě při povolování 2FA", + "otpSetupCheckCode": "Zadejte 6místný kód", + "otpSetupCheckCodeRetry": "Neplatný kód. Zkuste to prosím znovu.", + "otpSetup": "Povolit dvoufaktorové ověření", + "otpSetupDescription": "Zabezpečte svůj účet s další vrstvou ochrany", + "otpSetupScanQr": "Naskenujte tento QR kód pomocí vaší autentizační aplikace nebo zadejte tajný klíč ručně:", + "otpSetupSecretCode": "Ověřovací kód", + "otpSetupSuccess": "Dvoufázové ověřování povoleno", + "otpSetupSuccessStoreBackupCodes": "Váš účet je nyní bezpečnější. Nezapomeňte uložit své záložní kódy.", + "otpErrorDisable": "2FA nelze zakázat", + "otpErrorDisableDescription": "Došlo k chybě při zakázání 2FA", + "otpRemove": "Zakázat dvoufaktorové ověřování", + "otpRemoveDescription": "Zakázat dvoufaktorové ověřování vašeho účtu", + "otpRemoveSuccess": "Dvoufázové ověřování zakázáno", + "otpRemoveSuccessMessage": "Dvoufaktorové ověřování bylo pro váš účet zakázáno. Můžete jej kdykoliv znovu povolit.", + "otpRemoveSubmit": "Zakázat 2FA", + "paginator": "Strana {current} z {last}", + "paginatorToFirst": "Přejít na první stránku", + "paginatorToPrevious": "Přejít na předchozí stránku", + "paginatorToNext": "Přejít na další stránku", + "paginatorToLast": "Přejít na poslední stránku", + "copyText": "Kopírovat text", + "copyTextFailed": "Nepodařilo se zkopírovat text: ", + "copyTextClipboard": "Kopírovat do schránky", + "inviteErrorInvalidConfirmation": "Neplatné potvrzení", + "passwordRequired": "Heslo je vyžadováno", + "allowAll": "Povolit vše", + "permissionsAllowAll": "Povolit všechna oprávnění", + "githubUsernameRequired": "Je vyžadováno uživatelské jméno GitHub", + "supportKeyRequired": "Je vyžadován klíč podpory", + "passwordRequirementsChars": "Heslo musí mít alespoň 8 znaků", + "language": "Jazyk", + "verificationCodeRequired": "Kód je povinný", + "userErrorNoUpdate": "Žádný uživatel k aktualizaci", + "siteErrorNoUpdate": "Žádná stránka k aktualizaci", + "resourceErrorNoUpdate": "Žádný zdroj k aktualizaci", + "authErrorNoUpdate": "Žádné informace o ověření k aktualizaci", + "orgErrorNoUpdate": "Žádný z orgů k aktualizaci", + "orgErrorNoProvided": "Není k dispozici žádný org", + "apiKeysErrorNoUpdate": "Žádný API klíč k aktualizaci", + "sidebarOverview": "Přehled", + "sidebarHome": "Domů", + "sidebarSites": "Stránky", + "sidebarResources": "Zdroje", + "sidebarAccessControl": "Kontrola přístupu", + "sidebarUsers": "Uživatelé", + "sidebarInvitations": "Pozvánky", + "sidebarRoles": "Role", + "sidebarShareableLinks": "Sdílené odkazy", + "sidebarApiKeys": "API klíče", + "sidebarSettings": "Nastavení", + "sidebarAllUsers": "Všichni uživatelé", + "sidebarIdentityProviders": "Poskytovatelé identity", + "sidebarLicense": "Licence", + "sidebarClients": "Clients", + "sidebarDomains": "Domény", + "enableDockerSocket": "Povolit Docker plán", + "enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.", + "enableDockerSocketLink": "Zjistit více", + "viewDockerContainers": "Zobrazit kontejnery Dockeru", + "containersIn": "Kontejnery v {siteName}", + "selectContainerDescription": "Vyberte jakýkoli kontejner pro použití jako název hostitele pro tento cíl. Klikněte na port pro použití portu.", + "containerName": "Jméno", + "containerImage": "Obrázek", + "containerState": "Stát", + "containerNetworks": "Síť", "containerHostnameIp": "Hostname/IP", - "containerLabels": "Labels", - "containerLabelsCount": "{count, plural, one {# label} other {# labels}}", - "containerLabelsTitle": "Container Labels", + "containerLabels": "Popisky", + "containerLabelsCount": "{count, plural, one {# štítek} other {# štítků}}", + "containerLabelsTitle": "Popisky kontejneru", "containerLabelEmpty": "", - "containerPorts": "Ports", - "containerPortsMore": "+{count} more", - "containerActions": "Actions", - "select": "Select", - "noContainersMatchingFilters": "No containers found matching the current filters.", - "showContainersWithoutPorts": "Show containers without ports", - "showStoppedContainers": "Show stopped containers", - "noContainersFound": "No containers found. Make sure Docker containers are running.", - "searchContainersPlaceholder": "Search across {count} containers...", - "searchResultsCount": "{count, plural, one {# result} other {# results}}", - "filters": "Filters", - "filterOptions": "Filter Options", - "filterPorts": "Ports", - "filterStopped": "Stopped", - "clearAllFilters": "Clear all filters", - "columns": "Columns", - "toggleColumns": "Toggle Columns", - "refreshContainersList": "Refresh containers list", - "searching": "Searching...", - "noContainersFoundMatching": "No containers found matching \"{filter}\".", - "light": "light", - "dark": "dark", - "system": "system", - "theme": "Theme", - "subnetRequired": "Subnet is required", - "initialSetupTitle": "Initial Server Setup", - "initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.", - "createAdminAccount": "Create Admin Account", - "setupErrorCreateAdmin": "An error occurred while creating the server admin account.", - "certificateStatus": "Certificate Status", - "loading": "Loading", - "restart": "Restart", - "domains": "Domains", - "domainsDescription": "Manage domains for your organization", - "domainsSearch": "Search domains...", - "domainAdd": "Add Domain", - "domainAddDescription": "Register a new domain with your organization", - "domainCreate": "Create Domain", - "domainCreatedDescription": "Domain created successfully", - "domainDeletedDescription": "Domain deleted successfully", - "domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?", - "domainMessageRemove": "Once removed, the domain will no longer be associated with your account.", - "domainMessageConfirm": "To confirm, please type the domain name below.", - "domainConfirmDelete": "Confirm Delete Domain", - "domainDelete": "Delete Domain", - "domain": "Domain", - "selectDomainTypeNsName": "Domain Delegation (NS)", - "selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.", - "selectDomainTypeCnameName": "Single Domain (CNAME)", - "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", - "selectDomainTypeWildcardName": "Wildcard Domain", - "selectDomainTypeWildcardDescription": "This domain and its subdomains.", - "domainDelegation": "Single Domain", - "selectType": "Select a type", - "actions": "Actions", - "refresh": "Refresh", - "refreshError": "Failed to refresh data", - "verified": "Verified", - "pending": "Pending", - "sidebarBilling": "Billing", - "billing": "Billing", - "orgBillingDescription": "Manage your billing information and subscriptions", + "containerPorts": "Přístavy", + "containerPortsMore": "+{count} další", + "containerActions": "Akce", + "select": "Vybrat", + "noContainersMatchingFilters": "Nebyly nalezeny žádné kontejnery odpovídající aktuálním filtrům.", + "showContainersWithoutPorts": "Zobrazit kontejnery bez portů", + "showStoppedContainers": "Zobrazit zastavené kontejnery", + "noContainersFound": "Nebyly nalezeny žádné kontejnery. Ujistěte se, že jsou v kontejnerech Docker spuštěny.", + "searchContainersPlaceholder": "Hledat napříč {count} kontejnery...", + "searchResultsCount": "{count, plural, one {# výsledek} other {# výsledků}}", + "filters": "Filtry", + "filterOptions": "Možnosti filtru", + "filterPorts": "Přístavy", + "filterStopped": "Zastaveno", + "clearAllFilters": "Vymazat všechny filtry", + "columns": "Sloupce", + "toggleColumns": "Přepnout sloupce", + "refreshContainersList": "Aktualizovat seznam kontejnerů", + "searching": "Vyhledávání...", + "noContainersFoundMatching": "Nebyly nalezeny žádné kontejnery odpovídající \"{filter}\".", + "light": "lehké", + "dark": "tmavé", + "system": "systém", + "theme": "Téma", + "subnetRequired": "Podsíť je vyžadována", + "initialSetupTitle": "Počáteční nastavení serveru", + "initialSetupDescription": "Vytvořte účet správce intial serveru. Pouze jeden správce serveru může existovat. Tyto přihlašovací údaje můžete kdykoliv změnit.", + "createAdminAccount": "Vytvořit účet správce", + "setupErrorCreateAdmin": "Došlo k chybě při vytváření účtu správce serveru.", + "certificateStatus": "Stav certifikátu", + "loading": "Načítání", + "restart": "Restartovat", + "domains": "Domény", + "domainsDescription": "Spravovat domény pro vaši organizaci", + "domainsSearch": "Hledat domény...", + "domainAdd": "Přidat doménu", + "domainAddDescription": "Registrujte novou doménu ve vaší organizaci", + "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 {domain} z vašeho účtu?", + "domainMessageRemove": "Po odstranění domény již nebude přiřazena k vašemu účtu.", + "domainMessageConfirm": "Pro potvrzení zadejte název domény níže.", + "domainConfirmDelete": "Potvrdit odstranění domény", + "domainDelete": "Odstranit doménu", + "domain": "Doména", + "selectDomainTypeNsName": "Delegace domény (blíže neurčeno)", + "selectDomainTypeNsDescription": "Tato doména a všechny její subdomény. Použijte tuto možnost, pokud chcete ovládat celou doménu zóny.", + "selectDomainTypeCnameName": "Jedna doména (CNAME)", + "selectDomainTypeCnameDescription": "Jen tato konkrétní doména. Použijte tuto možnost pro jednotlivé subdomény nebo konkrétní doménové položky.", + "selectDomainTypeWildcardName": "Doména zástupného znaku", + "selectDomainTypeWildcardDescription": "Tato doména a její subdomény.", + "domainDelegation": "Jedna doména", + "selectType": "Vyberte typ", + "actions": "Akce", + "refresh": "Aktualizovat", + "refreshError": "Obnovení dat se nezdařilo", + "verified": "Ověřeno", + "pending": "Nevyřízeno", + "sidebarBilling": "Fakturace", + "billing": "Fakturace", + "orgBillingDescription": "Spravujte své fakturační údaje a předplatné", "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", - "fossorial": "Fossorial", - "completeAccountSetup": "Complete Account Setup", - "completeAccountSetupDescription": "Set your password to get started", - "accountSetupSent": "We'll send an account setup code to this email address.", - "accountSetupCode": "Setup Code", - "accountSetupCodeDescription": "Check your email for the setup code.", - "passwordCreate": "Create Password", - "passwordCreateConfirm": "Confirm Password", - "accountSetupSubmit": "Send Setup Code", - "completeSetup": "Complete Setup", - "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", - "documentation": "Documentation", - "saveAllSettings": "Save All Settings", - "settingsUpdated": "Settings updated", - "settingsUpdatedDescription": "All settings have been updated successfully", - "settingsErrorUpdate": "Failed to update settings", - "settingsErrorUpdateDescription": "An error occurred while updating settings", - "sidebarCollapse": "Collapse", - "sidebarExpand": "Expand", - "newtUpdateAvailable": "Update Available", - "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Domain", + "pangolinHosted": "Pangolin hostovaný", + "fossorial": "Fossorální", + "completeAccountSetup": "Dokončit nastavení účtu", + "completeAccountSetupDescription": "Nastavte heslo pro spuštění", + "accountSetupSent": "Na tuto e-mailovou adresu zašleme nastavovací kód účtu.", + "accountSetupCode": "Nastavte kód", + "accountSetupCodeDescription": "Zkontrolujte svůj e-mail pro nastavení.", + "passwordCreate": "Vytvořit heslo", + "passwordCreateConfirm": "Potvrďte heslo", + "accountSetupSubmit": "Odeslat instalační kód", + "completeSetup": "Dokončit nastavení", + "accountSetupSuccess": "Nastavení účtu dokončeno! Vítejte v Pangolinu!", + "documentation": "Dokumentace", + "saveAllSettings": "Uložit všechna nastavení", + "settingsUpdated": "Nastavení aktualizováno", + "settingsUpdatedDescription": "Všechna nastavení byla úspěšně aktualizována", + "settingsErrorUpdate": "Aktualizace nastavení se nezdařila", + "settingsErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení", + "sidebarCollapse": "Sbalit", + "sidebarExpand": "Rozbalit", + "newtUpdateAvailable": "Dostupná aktualizace", + "newtUpdateAvailableInfo": "Je k dispozici nová verze Newt. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.", + "domainPickerEnterDomain": "Doména", "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "Enter the full domain of the resource to see available options.", - "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", - "domainPickerTabAll": "All", - "domainPickerTabOrganization": "Organization", - "domainPickerTabProvided": "Provided", + "domainPickerDescription": "Zadejte úplnou doménu zdroje pro zobrazení dostupných možností.", + "domainPickerDescriptionSaas": "Zadejte celou doménu, subdoménu nebo jméno pro zobrazení dostupných možností", + "domainPickerTabAll": "Vše", + "domainPickerTabOrganization": "Organizace", + "domainPickerTabProvided": "Poskytnuto", "domainPickerSortAsc": "A-Z", "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", - "domainPickerOrganizationDomains": "Organization Domains", - "domainPickerProvidedDomains": "Provided Domains", - "domainPickerSubdomain": "Subdomain: {subdomain}", - "domainPickerNamespace": "Namespace: {namespace}", - "domainPickerShowMore": "Show More", - "domainNotFound": "Domain Not Found", - "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", - "failed": "Failed", - "createNewOrgDescription": "Create a new organization", - "organization": "Organization", - "port": "Port", - "securityKeyManage": "Manage Security Keys", - "securityKeyDescription": "Add or remove security keys for passwordless authentication", - "securityKeyRegister": "Register New Security Key", - "securityKeyList": "Your Security Keys", - "securityKeyNone": "No security keys registered yet", - "securityKeyNameRequired": "Name is required", - "securityKeyRemove": "Remove", - "securityKeyLastUsed": "Last used: {date}", - "securityKeyNameLabel": "Security Key Name", - "securityKeyRegisterSuccess": "Security key registered successfully", - "securityKeyRegisterError": "Failed to register security key", - "securityKeyRemoveSuccess": "Security key removed successfully", - "securityKeyRemoveError": "Failed to remove security key", - "securityKeyLoadError": "Failed to load security keys", - "securityKeyLogin": "Continue with security key", - "securityKeyAuthError": "Failed to authenticate with security key", - "securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.", - "registering": "Registering...", - "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", - "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", - "securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.", - "securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.", - "securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.", - "securityKeyUnknownError": "There was a problem using your security key. Please try again.", - "twoFactorRequired": "Two-factor authentication is required to register a security key.", - "twoFactor": "Two-Factor Authentication", - "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", - "continueToApplication": "Continue to Application", - "securityKeyAdd": "Add Security Key", - "securityKeyRegisterTitle": "Register New Security Key", - "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", - "securityKeyTwoFactorRequired": "Two-Factor Authentication Required", - "securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key", - "securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key", - "securityKeyTwoFactorCode": "Two-Factor Code", - "securityKeyRemoveTitle": "Remove Security Key", - "securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"", - "securityKeyNoKeysRegistered": "No security keys registered", - "securityKeyNoKeysDescription": "Add a security key to enhance your account security", - "createDomainRequired": "Domain is required", - "createDomainAddDnsRecords": "Add DNS Records", - "createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.", - "createDomainNsRecords": "NS Records", - "createDomainRecord": "Record", - "createDomainType": "Type:", - "createDomainName": "Name:", - "createDomainValue": "Value:", - "createDomainCnameRecords": "CNAME Records", - "createDomainARecords": "A Records", - "createDomainRecordNumber": "Record {number}", - "createDomainTxtRecords": "TXT Records", - "createDomainSaveTheseRecords": "Save These Records", - "createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.", - "createDomainDnsPropagation": "DNS Propagation", - "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", - "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "domainPickerCheckingAvailability": "Kontrola dostupnosti...", + "domainPickerNoMatchingDomains": "Nebyly nalezeny žádné odpovídající domény. Zkuste jinou doménu nebo zkontrolujte nastavení domény vaší organizace.", + "domainPickerOrganizationDomains": "Domény organizace", + "domainPickerProvidedDomains": "Poskytnuté domény", + "domainPickerSubdomain": "Subdoména: {subdomain}", + "domainPickerNamespace": "Jmenný prostor: {namespace}", + "domainPickerShowMore": "Zobrazit více", + "regionSelectorTitle": "Vybrat region", + "regionSelectorInfo": "Výběr regionu nám pomáhá poskytovat lepší výkon pro vaši polohu. Nemusíte být ve stejném regionu jako váš server.", + "regionSelectorPlaceholder": "Vyberte region", + "regionSelectorComingSoon": "Již brzy", + "billingLoadingSubscription": "Načítání odběru...", + "billingFreeTier": "Volná úroveň", + "billingWarningOverLimit": "Upozornění: Překročili jste jeden nebo více omezení používání. Vaše stránky se nepřipojí dokud nezměníte předplatné nebo neupravíte své používání.", + "billingUsageLimitsOverview": "Přehled omezení použití", + "billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@fossorial.io.", + "billingDataUsage": "Využití dat", + "billingOnlineTime": "Stránka online čas", + "billingUsers": "Aktivní uživatelé", + "billingDomains": "Aktivní domény", + "billingRemoteExitNodes": "Aktivní Samostatně hostované uzly", + "billingNoLimitConfigured": "Žádný limit nenastaven", + "billingEstimatedPeriod": "Odhadované období fakturace", + "billingIncludedUsage": "Zahrnuto využití", + "billingIncludedUsageDescription": "Využití zahrnované s aktuálním plánem předplatného", + "billingFreeTierIncludedUsage": "Povolenky bezplatné úrovně využití", + "billingIncluded": "zahrnuto", + "billingEstimatedTotal": "Odhadovaný celkem:", + "billingNotes": "Poznámky", + "billingEstimateNote": "Toto je odhad založený na aktuálním využití.", + "billingActualChargesMayVary": "Skutečné náklady se mohou lišit.", + "billingBilledAtEnd": "Budete účtováni na konci fakturační doby.", + "billingModifySubscription": "Upravit předplatné", + "billingStartSubscription": "Začít předplatné", + "billingRecurringCharge": "Opakované nabití", + "billingManageSubscriptionSettings": "Spravovat nastavení a nastavení předplatného", + "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í", + "billingFailedToGetCheckoutUrl": "Nepodařilo se získat adresu URL pokladny", + "billingPleaseTryAgainLater": "Zkuste to prosím znovu později.", + "billingCheckoutError": "Chyba pokladny", + "billingFailedToGetPortalUrl": "Nepodařilo se získat URL portálu", + "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.", + "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", + "createNewOrgDescription": "Vytvořit novou organizaci", + "organization": "Organizace", + "port": "Přístav", + "securityKeyManage": "Správa bezpečnostních klíčů", + "securityKeyDescription": "Přidat nebo odebrat bezpečnostní klíče pro bezheslou autentizaci", + "securityKeyRegister": "Registrovat nový bezpečnostní klíč", + "securityKeyList": "Vaše bezpečnostní klíče", + "securityKeyNone": "Zatím nejsou registrovány žádné bezpečnostní klíče", + "securityKeyNameRequired": "Název je povinný", + "securityKeyRemove": "Odebrat", + "securityKeyLastUsed": "Naposledy použito: {date}", + "securityKeyNameLabel": "Název bezpečnostního klíče", + "securityKeyRegisterSuccess": "Bezpečnostní klíč byl úspěšně zaregistrován", + "securityKeyRegisterError": "Nepodařilo se zaregistrovat bezpečnostní klíč", + "securityKeyRemoveSuccess": "Bezpečnostní klíč byl úspěšně odstraněn", + "securityKeyRemoveError": "Odstranění bezpečnostního klíče se nezdařilo", + "securityKeyLoadError": "Nepodařilo se načíst bezpečnostní klíče", + "securityKeyLogin": "Pokračovat s bezpečnostním klíčem", + "securityKeyAuthError": "Ověření bezpečnostním klíčem se nezdařilo", + "securityKeyRecommendation": "Registrujte záložní bezpečnostní klíč na jiném zařízení, abyste zajistili, že budete mít vždy přístup ke svému účtu.", + "registering": "Registrace...", + "securityKeyPrompt": "Ověřte svou identitu pomocí bezpečnostního klíče. Ujistěte se, že je Váš bezpečnostní klíč připojen a připraven.", + "securityKeyBrowserNotSupported": "Váš prohlížeč nepodporuje bezpečnostní klíče. Použijte prosím moderní prohlížeč jako Chrome, Firefox nebo Safari.", + "securityKeyPermissionDenied": "Prosím povolte přístup k bezpečnostnímu klíči, abyste mohli pokračovat v přihlašování.", + "securityKeyRemovedTooQuickly": "Prosím udržujte svůj bezpečnostní klíč připojený do dokončení procesu přihlášení.", + "securityKeyNotSupported": "Váš bezpečnostní klíč nemusí být kompatibilní. Zkuste jiný bezpečnostní klíč.", + "securityKeyUnknownError": "Vyskytl se problém s použitím bezpečnostního klíče. Zkuste to prosím znovu.", + "twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.", + "twoFactor": "Dvoufaktorové ověření", + "adminEnabled2FaOnYourAccount": "Váš správce povolil dvoufaktorové ověřování pro {email}. Chcete-li pokračovat, dokončete proces nastavení.", + "securityKeyAdd": "Přidat bezpečnostní klíč", + "securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč", + "securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci", + "securityKeyTwoFactorRequired": "Vyžadováno dvoufaktorové ověření", + "securityKeyTwoFactorDescription": "Zadejte prosím váš dvoufaktorový ověřovací kód pro registraci bezpečnostního klíče", + "securityKeyTwoFactorRemoveDescription": "Zadejte prosím váš dvoufaktorový ověřovací kód pro odstranění bezpečnostního klíče", + "securityKeyTwoFactorCode": "Dvoufaktorový kód", + "securityKeyRemoveTitle": "Odstranit bezpečnostní klíč", + "securityKeyRemoveDescription": "Zadejte své heslo pro odstranění bezpečnostního klíče \"{name}\"", + "securityKeyNoKeysRegistered": "Nebyly registrovány žádné bezpečnostní klíče", + "securityKeyNoKeysDescription": "Přidejte bezpečnostní klíč pro zvýšení zabezpečení vašeho účtu", + "createDomainRequired": "Doména je vyžadována", + "createDomainAddDnsRecords": "Přidat DNS záznamy", + "createDomainAddDnsRecordsDescription": "Přidejte následující DNS záznamy k poskytovateli domény pro dokončení nastavení.", + "createDomainNsRecords": "NS záznamy", + "createDomainRecord": "Nahrávat", + "createDomainType": "Typ:", + "createDomainName": "Jméno:", + "createDomainValue": "Hodnota:", + "createDomainCnameRecords": "Záznamy CNAME", + "createDomainARecords": "Záznamy", + "createDomainRecordNumber": "Nahrát {number}", + "createDomainTxtRecords": "TXT záznamy", + "createDomainSaveTheseRecords": "Uložit tyto záznamy", + "createDomainSaveTheseRecordsDescription": "Ujistěte se, že chcete uložit tyto DNS záznamy, protože je znovu neuvidíte.", + "createDomainDnsPropagation": "Šíření DNS", + "createDomainDnsPropagationDescription": "Změna DNS může trvat nějakou dobu, než se šíří po internetu. To může trvat kdekoli od několika minut do 48 hodin v závislosti na poskytovateli DNS a nastavení TTL.", + "resourcePortRequired": "Pro neHTTP zdroje je vyžadováno číslo portu", + "resourcePortNotAllowed": "Číslo portu by nemělo být nastaveno pro HTTP zdroje", + "billingPricingCalculatorLink": "Cenová kalkulačka", "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" + "IAgreeToThe": "Souhlasím s", + "termsOfService": "podmínky služby", + "and": "a", + "privacyPolicy": "zásady ochrany osobních údajů" }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", + "siteRequired": "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", + "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í", + "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 Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", - "resourceEnableProxy": "Enable Public Proxy", - "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled", + "olmSecretKey": "Olm tajný klíč", + "clientCredentialsSave": "Uložit přihlašovací údaje", + "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", + "clientUpdatedDescription": "Klient byl aktualizován.", + "clientUpdateFailed": "Nepodařilo se aktualizovat klienta", + "clientUpdateError": "Došlo k chybě při aktualizaci klienta.", + "sitesFetchFailed": "Nepodařilo se načíst stránky", + "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", "addNewTarget": "Add New Target", - "targetsList": "Targets List", - "targetErrorDuplicateTargetFound": "Duplicate target found", - "httpMethod": "HTTP Method", - "selectHttpMethod": "Select HTTP method", - "domainPickerSubdomainLabel": "Subdomain", - "domainPickerBaseDomainLabel": "Base Domain", - "domainPickerSearchDomains": "Search domains...", - "domainPickerNoDomainsFound": "No domains found", - "domainPickerLoadingDomains": "Loading domains...", - "domainPickerSelectBaseDomain": "Select base domain...", - "domainPickerNotAvailableForCname": "Not available for CNAME domains", - "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", - "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", - "domainPickerFreeDomains": "Free Domains", - "domainPickerSearchForAvailableDomains": "Search for available domains", - "resourceDomain": "Domain", - "resourceEditDomain": "Edit Domain", - "siteName": "Site Name", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy Resources", - "resourcesTableClientResources": "Client Resources", - "resourcesTableNoProxyResourcesFound": "No proxy resources found.", - "resourcesTableNoInternalResourcesFound": "No internal resources found.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "These resources are for use with", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", - "editInternalResourceDialogEditClientResource": "Edit Client Resource", - "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Resource Properties", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Port", + "targetsList": "Seznam cílů", + "advancedMode": "Pokročilý režim", + "targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl", + "healthCheckHealthy": "Zdravé", + "healthCheckUnhealthy": "Nezdravé", + "healthCheckUnknown": "Neznámý", + "healthCheck": "Kontrola stavu", + "configureHealthCheck": "Konfigurace kontroly stavu", + "configureHealthCheckDescription": "Nastavit sledování zdravotního stavu pro {target}", + "enableHealthChecks": "Povolit kontrolu stavu", + "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", + "healthCheckPath": "Cesta", + "healthHostname": "IP / Hostitel", + "healthPort": "Přístav", + "healthCheckPathDescription": "Cesta ke kontrole zdravotního stavu.", + "healthyIntervalSeconds": "Interval zdraví", + "unhealthyIntervalSeconds": "Nezdravý interval", + "IntervalSeconds": "Interval zdraví", + "timeoutSeconds": "Časový limit", + "timeIsInSeconds": "Čas je v sekundách", + "retryAttempts": "Opakovat pokusy", + "expectedResponseCodes": "Očekávané kódy odezvy", + "expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.", + "customHeaders": "Vlastní záhlaví", + "customHeadersDescription": "Záhlaví oddělená nová řádka: hodnota", + "headersValidationError": "Headers must be in the format: Header-Name: value.", + "saveHealthCheck": "Uložit kontrolu stavu", + "healthCheckSaved": "Kontrola stavu uložena", + "healthCheckSavedDescription": "Nastavení kontroly stavu bylo úspěšně uloženo", + "healthCheckError": "Chyba kontroly stavu", + "healthCheckErrorDescription": "Došlo k chybě při ukládání konfigurace kontroly stavu", + "healthCheckPathRequired": "Je vyžadována cesta kontroly stavu", + "healthCheckMethodRequired": "HTTP metoda je povinná", + "healthCheckIntervalMin": "Interval kontroly musí být nejméně 5 sekund", + "healthCheckTimeoutMin": "Časový limit musí být nejméně 1 sekunda", + "healthCheckRetryMin": "Pokusy opakovat musí být alespoň 1", + "httpMethod": "HTTP metoda", + "selectHttpMethod": "Vyberte HTTP metodu", + "domainPickerSubdomainLabel": "Subdoména", + "domainPickerBaseDomainLabel": "Základní doména", + "domainPickerSearchDomains": "Hledat domény...", + "domainPickerNoDomainsFound": "Nebyly nalezeny žádné domény", + "domainPickerLoadingDomains": "Načítám domény...", + "domainPickerSelectBaseDomain": "Vyberte základní doménu...", + "domainPickerNotAvailableForCname": "Není k dispozici pro domény CNAME", + "domainPickerEnterSubdomainOrLeaveBlank": "Zadejte subdoménu nebo ponechte prázdné pro použití základní domény.", + "domainPickerEnterSubdomainToSearch": "Zadejte subdoménu pro hledání a výběr z dostupných domén zdarma.", + "domainPickerFreeDomains": "Volné domény", + "domainPickerSearchForAvailableDomains": "Hledat dostupné domény", + "domainPickerNotWorkSelfHosted": "Poznámka: Poskytnuté domény nejsou momentálně k dispozici pro vlastní hostované instance.", + "resourceDomain": "Doména", + "resourceEditDomain": "Upravit doménu", + "siteName": "Název webu", + "proxyPort": "Přístav", + "resourcesTableProxyResources": "Zdroje proxy", + "resourcesTableClientResources": "Zdroje klienta", + "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", + "resourcesTableClients": "Klienti", + "resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.", + "editInternalResourceDialogEditClientResource": "Upravit klientský dokument", + "editInternalResourceDialogUpdateResourceProperties": "Aktualizujte vlastnosti zdroje a cílovou konfiguraci pro {resourceName}.", + "editInternalResourceDialogResourceProperties": "Vlastnosti zdroje", + "editInternalResourceDialogName": "Jméno", + "editInternalResourceDialogProtocol": "Protokol", + "editInternalResourceDialogSitePort": "Port webu", "editInternalResourceDialogTargetConfiguration": "Target Configuration", - "editInternalResourceDialogCancel": "Cancel", - "editInternalResourceDialogSaveResource": "Save Resource", - "editInternalResourceDialogSuccess": "Success", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", - "editInternalResourceDialogNameRequired": "Name is required", - "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "createInternalResourceDialogNoSitesAvailable": "No Sites Available", - "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", - "createInternalResourceDialogClose": "Close", - "createInternalResourceDialogCreateClientResource": "Create Client Resource", - "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", - "createInternalResourceDialogResourceProperties": "Resource Properties", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", - "createInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogCancel": "Zrušit", + "editInternalResourceDialogSaveResource": "Uložit dokument", + "editInternalResourceDialogSuccess": "Úspěšně", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interní zdroj byl úspěšně aktualizován", + "editInternalResourceDialogError": "Chyba", + "editInternalResourceDialogFailedToUpdateInternalResource": "Aktualizace interního zdroje se nezdařila", + "editInternalResourceDialogNameRequired": "Název je povinný", + "editInternalResourceDialogNameMaxLength": "Název musí mít méně než 255 znaků", + "editInternalResourceDialogProxyPortMin": "Port proxy serveru musí být alespoň 1", + "editInternalResourceDialogProxyPortMax": "Port proxy serveru musí být menší než 65536", + "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", + "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.", + "createInternalResourceDialogResourceProperties": "Vlastnosti zdroje", + "createInternalResourceDialogName": "Jméno", + "createInternalResourceDialogSite": "Lokalita", + "createInternalResourceDialogSelectSite": "Vybrat lokalitu...", + "createInternalResourceDialogSearchSites": "Hledat lokality...", + "createInternalResourceDialogNoSitesFound": "Nebyly nalezeny žádné stránky.", + "createInternalResourceDialogProtocol": "Protokol", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Port", - "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogSitePort": "Port webu", + "createInternalResourceDialogSitePortDescription": "Použijte tento port pro přístup ke zdroji na webu při připojení s klientem.", "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.", - "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", - "createInternalResourceDialogCancel": "Cancel", - "createInternalResourceDialogCreateResource": "Create Resource", - "createInternalResourceDialogSuccess": "Success", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", - "createInternalResourceDialogNameRequired": "Name is required", - "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", - "createInternalResourceDialogPleaseSelectSite": "Please select a site", - "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", - "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", - "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", - "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept Client Connections", - "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", - "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.", - "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", - "selectIdp": "Select IDP", - "selectIdpPlaceholder": "Choose an IDP...", - "selectIdpRequired": "Please select an IDP when auto login is enabled.", - "autoLoginTitle": "Redirecting", - "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", - "autoLoginProcessing": "Preparing authentication...", - "autoLoginRedirecting": "Redirecting to login...", - "autoLoginError": "Auto Login Error", - "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", - "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", + "createInternalResourceDialogDestinationIPDescription": "IP nebo název hostitele zdroje v síti webu.", + "createInternalResourceDialogDestinationPortDescription": "Přístav na cílové IP adrese, kde je zdroj dostupný.", + "createInternalResourceDialogCancel": "Zrušit", + "createInternalResourceDialogCreateResource": "Vytvořit zdroj", + "createInternalResourceDialogSuccess": "Úspěšně", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interní zdroj byl úspěšně vytvořen", + "createInternalResourceDialogError": "Chyba", + "createInternalResourceDialogFailedToCreateInternalResource": "Nepodařilo se vytvořit interní zdroj", + "createInternalResourceDialogNameRequired": "Název je povinný", + "createInternalResourceDialogNameMaxLength": "Název musí mít méně než 255 znaků", + "createInternalResourceDialogPleaseSelectSite": "Vyberte prosím web", + "createInternalResourceDialogProxyPortMin": "Port proxy serveru musí být alespoň 1", + "createInternalResourceDialogProxyPortMax": "Port proxy serveru musí být menší než 65536", + "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", + "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.", + "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", + "selectIdpPlaceholder": "Vyberte IDP...", + "selectIdpRequired": "Prosím vyberte IDP, když je povoleno automatické přihlášení.", + "autoLoginTitle": "Přesměrování", + "autoLoginDescription": "Přesměrování k externímu poskytovateli identity pro ověření.", + "autoLoginProcessing": "Příprava ověřování...", + "autoLoginRedirecting": "Přesměrování k přihlášení...", + "autoLoginError": "Automatická chyba přihlášení", + "autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.", + "autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.", + "remoteExitNodeManageRemoteExitNodes": "Vzdálené uzly", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Uzly", + "searchRemoteExitNodes": "Hledat uzly...", + "remoteExitNodeAdd": "Přidat uzel", + "remoteExitNodeErrorDelete": "Chyba při odstraňování uzlu", + "remoteExitNodeQuestionRemove": "Jste si jisti, že chcete odstranit uzel {selectedNode} z organizace?", + "remoteExitNodeMessageRemove": "Po odstranění uzel již nebude přístupný.", + "remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.", + "remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu", + "remoteExitNodeDelete": "Odstranit uzel", + "sidebarRemoteExitNodes": "Vzdálené uzly", + "remoteExitNodeCreate": { + "title": "Vytvořit uzel", + "description": "Vytvořit nový uzel pro rozšíření síťového připojení", + "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.", + "adopt": { + "title": "Přijmout uzel", + "description": "Zvolte tuto možnost, pokud již máte přihlašovací údaje k uzlu." + }, + "generate": { + "title": "Generovat klíče", + "description": "Vyberte tuto možnost, pokud chcete vygenerovat nové klíče pro uzel" + } + }, + "adopt": { + "title": "Přijmout existující uzel", + "description": "Zadejte přihlašovací údaje existujícího uzlu, který chcete přijmout", + "nodeIdLabel": "ID uzlu", + "nodeIdDescription": "ID existujícího uzlu, který chcete přijmout", + "secretLabel": "Tajný klíč", + "secretDescription": "Tajný klíč existujícího uzlu", + "submitButton": "Přijmout uzel" + }, + "generate": { + "title": "Vygenerovaná pověření", + "description": "Použijte tyto generované přihlašovací údaje pro nastavení vašeho uzlu", + "nodeIdTitle": "ID uzlu", + "secretTitle": "Tajný klíč", + "saveCredentialsTitle": "Přidat přihlašovací údaje do konfigurace", + "saveCredentialsDescription": "Přidejte tyto přihlašovací údaje do vlastního konfiguračního souboru Pangolin uzlu pro dokončení připojení.", + "submitButton": "Vytvořit uzel" + }, + "validation": { + "adoptRequired": "ID uzlu a tajný klíč jsou vyžadovány při přijetí existujícího uzlu" + }, + "errors": { + "loadDefaultsFailed": "Nepodařilo se načíst výchozí hodnoty", + "defaultsNotLoaded": "Výchozí hodnoty nebyly načteny", + "createFailed": "Nepodařilo se vytvořit uzel" + }, + "success": { + "created": "Uzel byl úspěšně vytvořen" + } + }, + "remoteExitNodeSelection": "Výběr uzlu", + "remoteExitNodeSelectionDescription": "Vyberte uzel pro směrování provozu přes tuto lokální stránku", + "remoteExitNodeRequired": "Pro lokální stránky musí být vybrán uzel", + "noRemoteExitNodesAvailable": "Nejsou k dispozici žádné uzly", + "noRemoteExitNodesAvailableDescription": "Pro tuto organizaci nejsou k dispozici žádné uzly. Nejprve vytvořte uzel pro použití lokálních stránek.", + "exitNode": "Ukončit uzel", + "country": "L 343, 22.12.2009, s. 1).", + "rulesMatchCountry": "Aktuálně založené na zdrojové IP adrese", "managedSelfHosted": { - "title": "Managed Self-Hosted", - "description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles", - "introTitle": "Managed Self-Hosted Pangolin", - "introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.", - "introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:", + "title": "Spravované vlastní hostování", + "description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami", + "introTitle": "Spravovaný Pangolin", + "introDescription": "je možnost nasazení určená pro lidi, kteří chtějí jednoduchost a spolehlivost při zachování soukromých a samoobslužných dat.", + "introDetail": "Pomocí této volby stále provozujete vlastní uzel Pangolin — tunely, SSL terminály a provoz všech pobytů na vašem serveru. Rozdíl spočívá v tom, že řízení a monitorování se řeší prostřednictvím našeho cloudového panelu, který odemkne řadu výhod:", "benefitSimplerOperations": { - "title": "Simpler operations", - "description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box." + "title": "Jednoduchý provoz", + "description": "Není třeba spouštět svůj vlastní poštovní server nebo nastavit komplexní upozornění. Ze schránky dostanete upozornění na zdravotní kontrolu a výpadek." }, "benefitAutomaticUpdates": { - "title": "Automatic updates", - "description": "The cloud dashboard evolves quickly, so you get new features and bug fixes without having to manually pull new containers every time." + "title": "Automatické aktualizace", + "description": "Nástěnka cloudu se rychle vyvíjí, takže dostanete nové funkce a opravy chyb, aniž byste museli vždy ručně stahovat nové kontejnery." }, "benefitLessMaintenance": { - "title": "Less maintenance", - "description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud." + "title": "Méně údržby", + "description": "Žádná migrace do databáze, zálohování nebo další infrastruktura pro správu. Zabýváme se tím v cloudu." }, "benefitCloudFailover": { - "title": "Cloud failover", - "description": "If your node goes down, your tunnels can temporarily fail over to our cloud points of presence until you bring it back online." + "title": "Selhání cloudu", + "description": "Pokud váš uzel klesne, vaše tunely mohou dočasně selhat na naše body přítomnosti v cloudu, dokud jej nevrátíte zpět online." }, "benefitHighAvailability": { - "title": "High availability (PoPs)", - "description": "You can also attach multiple nodes to your account for redundancy and better performance." + "title": "Vysoká dostupnost (PoP)", + "description": "Můžete také připojit více uzlů k vašemu účtu pro nadbytečnost a lepší výkon." }, "benefitFutureEnhancements": { - "title": "Future enhancements", - "description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust." + "title": "Budoucí vylepšení", + "description": "Plánujeme přidat více analytických, varovných a manažerských nástrojů, aby bylo vaše nasazení ještě robustnější." }, "docsAlert": { - "text": "Learn more about the Managed Self-Hosted option in our", - "documentation": "documentation" + "text": "Další informace o možnostech Managed Self-Hosted v našem", + "documentation": "dokumentace" }, - "convertButton": "Convert This Node to Managed Self-Hosted" + "convertButton": "Převést tento uzel na spravovaný vlastní hostitel" }, - "internationaldomaindetected": "International Domain Detected", - "willbestoredas": "Will be stored as:", - "idpGoogleDescription": "Google OAuth2/OIDC provider", + "internationaldomaindetected": "Zjištěna mezinárodní doména", + "willbestoredas": "Bude uloženo jako:", + "roleMappingDescription": "Určete, jak jsou role přiřazeny uživatelům, když se přihlásí, když je povoleno automatické poskytnutí služby.", + "selectRole": "Vyberte roli", + "roleMappingExpression": "Výraz", + "selectRolePlaceholder": "Vyberte roli", + "selectRoleDescription": "Vyberte roli pro přiřazení všem uživatelům od tohoto poskytovatele identity", + "roleMappingExpressionDescription": "Zadejte výraz JMESPath pro získání informací o roli z ID token", + "idpTenantIdRequired": "ID nájemce je povinné", + "invalidValue": "Neplatná hodnota", + "idpTypeLabel": "Typ poskytovatele identity", + "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", + "idpGoogleClientSecretDescription": "Tajný klíč klienta Google OAuth2", + "idpAzureConfiguration": "Nastavení Azure Entra ID", + "idpAzureConfigurationDescription": "Nastavte vaše 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", + "idpAzureClientSecretDescription": "Tajný klíč registrace aplikace Azure", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Konfigurace Google", + "idpAzureConfigurationTitle": "Nastavení Azure Entra ID", + "idpTenantIdLabel": "ID tenanta", + "idpAzureClientIdDescription2": "Vaše ID registrace aplikace Azure", + "idpAzureClientSecretDescription2": "Tajný klíč registrace aplikace Azure", + "idpGoogleDescription": "Poskytovatel Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Custom Headers", - "headersValidationError": "Headers must be in the format: Header-Name: value.", - "domainPickerProvidedDomain": "Provided Domain", - "domainPickerFreeProvidedDomain": "Free Provided Domain", - "domainPickerVerified": "Verified", - "domainPickerUnverified": "Unverified", - "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", - "domainPickerError": "Error", - "domainPickerErrorLoadDomains": "Failed to load organization domains", - "domainPickerErrorCheckAvailability": "Failed to check domain availability", - "domainPickerInvalidSubdomain": "Invalid subdomain", - "domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.", - "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", - "domainPickerSubdomainSanitized": "Subdomain sanitized", - "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", - "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Edit file: docker-compose.yml" + "subnet": "Podsíť", + "subnetDescription": "Podsíť pro konfiguraci sítě této organizace.", + "authPage": "Auth stránka", + "authPageDescription": "Konfigurace autentizační stránky vaší organizace", + "authPageDomain": "Doména ověření stránky", + "noDomainSet": "Není nastavena žádná doména", + "changeDomain": "Změnit doménu", + "selectDomain": "Vybrat doménu", + "restartCertificate": "Restartovat certifikát", + "editAuthPageDomain": "Upravit doménu autentizační stránky", + "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", + "selectDomainForOrgAuthPage": "Vyberte doménu pro ověřovací stránku organizace", + "domainPickerProvidedDomain": "Poskytnutá doména", + "domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména", + "domainPickerVerified": "Ověřeno", + "domainPickerUnverified": "Neověřeno", + "domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.", + "domainPickerError": "Chyba", + "domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace", + "domainPickerErrorCheckAvailability": "Kontrola dostupnosti domény se nezdařila", + "domainPickerInvalidSubdomain": "Neplatná subdoména", + "domainPickerInvalidSubdomainRemoved": "Vstup \"{sub}\" byl odstraněn, protože není platný.", + "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", + "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", + "subscriptionRequiredToUse": "Pro použití této funkce je vyžadováno předplatné.", + "idpDisabled": "Poskytovatelé identit jsou zakázáni.", + "orgAuthPageDisabled": "Ověřovací stránka organizace je zakázána.", + "domainRestartedDescription": "Ověření domény bylo úspěšně restartováno", + "resourceAddEntrypointsEditFile": "Upravit soubor: config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "Upravit soubor: docker-compose.yml", + "emailVerificationRequired": "Je vyžadováno ověření e-mailu. Přihlaste se znovu pomocí {dashboardUrl}/auth/login dokončete tento krok. Poté se vraťte zde.", + "twoFactorSetupRequired": "Je vyžadováno nastavení dvoufaktorového ověřování. Přihlaste se znovu pomocí {dashboardUrl}/autentizace/přihlášení dokončí tento krok. Poté se vraťte zde.", + "authPageErrorUpdateMessage": "Při aktualizaci nastavení autentizační stránky došlo k chybě", + "authPageUpdated": "Autentizační stránka byla úspěšně aktualizována", + "healthCheckNotAvailable": "Místní", + "rewritePath": "Přepsat cestu", + "rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl.", + "continueToApplication": "Pokračovat v aplikaci", + "checkingInvite": "Kontrola pozvánky", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Odstranit Autentizaci Záhlaví", + "resourceHeaderAuthRemoveDescription": "Úspěšně odstraněna autentizace záhlaví.", + "resourceErrorHeaderAuthRemove": "Nepodařilo se odstranit Autentizaci Záhlaví", + "resourceErrorHeaderAuthRemoveDescription": "Nepodařilo se odstranit autentizaci záhlaví ze zdroje.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Nepodařilo se nastavit Autentizaci Záhlaví", + "resourceErrorHeaderAuthSetupDescription": "Nepodařilo se nastavit autentizaci záhlaví ze zdroje.", + "resourceHeaderAuthSetup": "Úspěšně nastavena Autentizace Záhlaví", + "resourceHeaderAuthSetupDescription": "Autentizace záhlaví byla úspěšně nastavena.", + "resourceHeaderAuthSetupTitle": "Nastavit Autentizaci Záhlaví", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Nastavit Autentizaci Záhlaví", + "actionSetResourceHeaderAuth": "Nastavit Autentizaci Záhlaví", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priorita", + "priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/de-DE.json b/messages/de-DE.json index 099bcff9..fa534c77 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -96,7 +96,7 @@ "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. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Alle Standorte anzeigen", "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", "siteNewtCredentials": "Neue Newt Zugangsdaten", @@ -168,6 +168,9 @@ "siteSelect": "Standort auswählen", "siteSearch": "Standorte durchsuchen", "siteNotFound": "Keinen Standort gefunden.", + "selectCountry": "Land auswählen", + "searchCountries": "Länder suchen...", + "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", @@ -465,7 +468,10 @@ "createdAt": "Erstellt am", "proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.", "proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.", - "proxyEnableSSL": "SSL aktivieren (https)", + "proxyEnableSSL": "SSL aktivieren", + "proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.", + "target": "Target", + "configureTarget": "Ziele konfigurieren", "targetErrorFetch": "Fehler beim Abrufen der Ziele", "targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten", "siteErrorFetch": "Fehler beim Abrufen der Ressource", @@ -492,7 +498,7 @@ "targetTlsSettings": "Sicherheitskonfiguration", "targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource", "targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen", - "targetTlsSni": "TLS-Servername (SNI)", + "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", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", "methodSelect": "Methode auswählen", "targetSubmit": "Ziel hinzufügen", - "targetNoOne": "Keine Ziele. Fügen Sie ein Ziel über das Formular hinzu.", + "targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.", "targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.", "targetsSubmit": "Ziele speichern", + "addTarget": "Ziel hinzufügen", + "targetErrorInvalidIp": "Ungültige IP-Adresse", + "targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein", + "targetErrorInvalidPort": "Ungültiger Port", + "targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein", + "targetErrorNoSite": "Keine Site ausgewählt", + "targetErrorNoSiteDescription": "Bitte wähle eine Seite für das Ziel aus", + "targetCreated": "Ziel erstellt", + "targetCreatedDescription": "Ziel wurde erfolgreich erstellt", + "targetErrorCreate": "Fehler beim Erstellen des Ziels", + "targetErrorCreateDescription": "Beim Erstellen des Ziels ist ein Fehler aufgetreten", + "save": "Speichern", "proxyAdditional": "Zusätzliche Proxy-Einstellungen", "proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht", "proxyCustomHeader": "Benutzerdefinierter Host-Header", @@ -513,7 +531,7 @@ "ipAddressErrorInvalidFormat": "Ungültiges IP-Adressformat", "ipAddressErrorInvalidOctet": "Ungültiges IP-Adress-Oktett", "path": "Pfad", - "matchPath": "Unterverzeichnis", + "matchPath": "Spielpfad", "ipAddressRange": "IP-Bereich", "rulesErrorFetch": "Fehler beim Abrufen der Regeln", "rulesErrorFetchDescription": "Beim Abrufen der Regeln ist ein Fehler aufgetreten", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Server-Admin - Pangolin", "licenseTierProfessional": "Professional Lizenz", "licenseTierEnterprise": "Enterprise Lizenz", - "licenseTierCommercial": "Gewerbliche Lizenz", + "licenseTierPersonal": "Personal License", "licensed": "Lizenziert", "yes": "Ja", "no": "Nein", @@ -747,7 +765,7 @@ "idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter", "idpAutoProvisionUsers": "Automatische Benutzerbereitstellung", "idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.", - "licenseBadge": "Profi", + "licenseBadge": "EE", "idpType": "Anbietertyp", "idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten", "idpOidcConfigure": "OAuth2/OIDC Konfiguration", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Verbunden", "idpErrorConnectingTo": "Es gab ein Problem bei der Verbindung zu {name}. Bitte kontaktieren Sie Ihren Administrator.", "idpErrorNotFound": "IdP nicht gefunden", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Ungültige Einladung", "inviteInvalidDescription": "Der Einladungslink ist ungültig.", "inviteErrorWrongUser": "Einladung ist nicht für diesen Benutzer", @@ -1083,7 +1099,6 @@ "navbar": "Navigationsmenü", "navbarDescription": "Hauptnavigationsmenü für die Anwendung", "navbarDocsLink": "Dokumentation", - "commercialEdition": "Kommerzielle Edition", "otpErrorEnable": "2FA konnte nicht aktiviert werden", "otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten", "otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein", @@ -1139,8 +1154,8 @@ "sidebarAllUsers": "Alle Benutzer", "sidebarIdentityProviders": "Identitätsanbieter", "sidebarLicense": "Lizenz", - "sidebarClients": "Clients (Beta)", - "sidebarDomains": "Domains", + "sidebarClients": "Clients", + "sidebarDomains": "Domänen", "enableDockerSocket": "Docker Blaupause aktivieren", "enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.", "enableDockerSocketLink": "Mehr erfahren", @@ -1188,7 +1203,7 @@ "certificateStatus": "Zertifikatsstatus", "loading": "Laden", "restart": "Neustart", - "domains": "Domains", + "domains": "Domänen", "domainsDescription": "Domains für Ihre Organisation verwalten", "domainsSearch": "Domains durchsuchen...", "domainAdd": "Domain hinzufügen", @@ -1201,7 +1216,7 @@ "domainMessageConfirm": "Um zu bestätigen, geben Sie bitte den Domainnamen unten ein.", "domainConfirmDelete": "Domain-Löschung bestätigen", "domainDelete": "Domain löschen", - "domain": "Domain", + "domain": "Domäne", "selectDomainTypeNsName": "Domain-Delegation (NS)", "selectDomainTypeNsDescription": "Diese Domain und alle ihre Subdomains. Verwenden Sie dies, wenn Sie eine gesamte Domainzone kontrollieren möchten.", "selectDomainTypeCnameName": "Einzelne Domain (CNAME)", @@ -1241,7 +1256,7 @@ "sidebarExpand": "Erweitern", "newtUpdateAvailable": "Update verfügbar", "newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.", - "domainPickerEnterDomain": "Domain", + "domainPickerEnterDomain": "Domäne", "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.", "domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Subdomain: {subdomain}", "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mehr anzeigen", + "regionSelectorTitle": "Region auswählen", + "regionSelectorInfo": "Das Auswählen einer Region hilft uns, eine bessere Leistung für Ihren Standort bereitzustellen. Sie müssen sich nicht in derselben Region wie Ihr Server befinden.", + "regionSelectorPlaceholder": "Wähle eine Region", + "regionSelectorComingSoon": "Kommt bald", + "billingLoadingSubscription": "Abonnement wird geladen...", + "billingFreeTier": "Kostenlose Stufe", + "billingWarningOverLimit": "Warnung: Sie haben ein oder mehrere Nutzungslimits überschritten. Ihre Webseiten werden nicht verbunden, bis Sie Ihr Abonnement ändern oder Ihren Verbrauch anpassen.", + "billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen", + "billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@fossorial.io.", + "billingDataUsage": "Datenverbrauch", + "billingOnlineTime": "Online-Zeit der Seite", + "billingUsers": "Aktive Benutzer", + "billingDomains": "Aktive Domänen", + "billingRemoteExitNodes": "Aktive selbstgehostete Nodes", + "billingNoLimitConfigured": "Kein Limit konfiguriert", + "billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum", + "billingIncludedUsage": "Inklusive Nutzung", + "billingIncludedUsageDescription": "Nutzung, die in Ihrem aktuellen Abonnementplan enthalten ist", + "billingFreeTierIncludedUsage": "Nutzungskontingente der kostenlosen Stufe", + "billingIncluded": "inbegriffen", + "billingEstimatedTotal": "Geschätzte Gesamtsumme:", + "billingNotes": "Notizen", + "billingEstimateNote": "Dies ist eine Schätzung basierend auf Ihrem aktuellen Verbrauch.", + "billingActualChargesMayVary": "Tatsächliche Kosten können variieren.", + "billingBilledAtEnd": "Sie werden am Ende des Abrechnungszeitraums in Rechnung gestellt.", + "billingModifySubscription": "Abonnement ändern", + "billingStartSubscription": "Abonnement starten", + "billingRecurringCharge": "Wiederkehrende Kosten", + "billingManageSubscriptionSettings": "Verwalten Sie Ihre Abonnement-Einstellungen und Präferenzen", + "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", + "billingFailedToGetCheckoutUrl": "Fehler beim Abrufen der Checkout-URL", + "billingPleaseTryAgainLater": "Bitte versuchen Sie es später noch einmal.", + "billingCheckoutError": "Checkout-Fehler", + "billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL", + "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 für jede Domäne in Ihrer Organisation berechnet. Die Abrechnung erfolgt täglich basierend auf der Anzahl der aktiven Domänenkonten 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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.", "twoFactor": "Zwei-Faktor-Authentifizierung", "adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.", - "continueToApplication": "Weiter zur Anwendung", "securityKeyAdd": "Sicherheitsschlüssel hinzufügen", "securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren", "securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Es kann einige Zeit dauern, bis DNS-Änderungen im Internet verbreitet werden. Dies kann je nach Ihrem DNS-Provider und den TTL-Einstellungen von einigen Minuten bis zu 48 Stunden dauern.", "resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich", "resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden", + "billingPricingCalculatorLink": "Preisrechner", "signUpTerms": { "IAgreeToThe": "Ich stimme den", "termsOfService": "Nutzungsbedingungen zu", @@ -1327,7 +1384,7 @@ "privacyPolicy": "Datenschutzrichtlinie" }, "siteRequired": "Standort ist erforderlich.", - "olmTunnel": "Olm Tunnel", + "olmTunnel": "Olm-Tunnel", "olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung", "errorCreatingClient": "Fehler beim Erstellen des Clients", "clientDefaultsNotFound": "Kundenvorgaben nicht gefunden", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Externer Proxy aktiviert", "addNewTarget": "Neues Ziel hinzufügen", "targetsList": "Ziel-Liste", + "advancedMode": "Erweiterter Modus", "targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden", + "healthCheckHealthy": "Gesund", + "healthCheckUnhealthy": "Ungesund", + "healthCheckUnknown": "Unbekannt", + "healthCheck": "Gesundheits-Check", + "configureHealthCheck": "Gesundheits-Check konfigurieren", + "configureHealthCheckDescription": "Richten Sie die Gesundheitsüberwachung für {target} ein", + "enableHealthChecks": "Gesundheits-Checks aktivieren", + "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", + "healthCheckPath": "Pfad", + "healthHostname": "IP / Host", + "healthPort": "Port", + "healthCheckPathDescription": "Der Pfad zum Überprüfen des Gesundheitszustands.", + "healthyIntervalSeconds": "Gesunder Intervall", + "unhealthyIntervalSeconds": "Ungesunder Intervall", + "IntervalSeconds": "Gesunder Intervall", + "timeoutSeconds": "Timeout", + "timeIsInSeconds": "Zeit ist in Sekunden", + "retryAttempts": "Wiederholungsversuche", + "expectedResponseCodes": "Erwartete Antwortcodes", + "expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.", + "customHeaders": "Eigene Kopfzeilen", + "customHeadersDescription": "Header neue Zeile getrennt: Header-Name: Wert", + "headersValidationError": "Header müssen im Format Header-Name: Wert sein.", + "saveHealthCheck": "Gesundheits-Check speichern", + "healthCheckSaved": "Gesundheits-Check gespeichert", + "healthCheckSavedDescription": "Die Konfiguration des Gesundheits-Checks wurde erfolgreich gespeichert", + "healthCheckError": "Fehler beim Gesundheits-Check", + "healthCheckErrorDescription": "Beim Speichern der Gesundheits-Check-Konfiguration ist ein Fehler aufgetreten", + "healthCheckPathRequired": "Gesundheits-Check-Pfad ist erforderlich", + "healthCheckMethodRequired": "HTTP-Methode ist erforderlich", + "healthCheckIntervalMin": "Prüfintervall muss mindestens 5 Sekunden betragen", + "healthCheckTimeoutMin": "Timeout muss mindestens 1 Sekunde betragen", + "healthCheckRetryMin": "Wiederholungsversuche müssen mindestens 1 betragen", "httpMethod": "HTTP-Methode", "selectHttpMethod": "HTTP-Methode auswählen", "domainPickerSubdomainLabel": "Subdomain", @@ -1381,7 +1474,8 @@ "domainPickerEnterSubdomainToSearch": "Geben Sie eine Subdomain ein, um verfügbare freie Domains zu suchen und auszuwählen.", "domainPickerFreeDomains": "Freie Domains", "domainPickerSearchForAvailableDomains": "Verfügbare Domains suchen", - "resourceDomain": "Domain", + "domainPickerNotWorkSelfHosted": "Hinweis: Kostenlose bereitgestellte Domains sind derzeit nicht für selbstgehostete Instanzen verfügbar.", + "resourceDomain": "Domäne", "resourceEditDomain": "Domain bearbeiten", "siteName": "Site-Name", "proxyPort": "Port", @@ -1463,6 +1557,72 @@ "autoLoginError": "Fehler bei der automatischen Anmeldung", "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.", + "remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Knoten", + "searchRemoteExitNodes": "Knoten suchen...", + "remoteExitNodeAdd": "Knoten hinzufügen", + "remoteExitNodeErrorDelete": "Fehler beim Löschen des Knotens", + "remoteExitNodeQuestionRemove": "Sind Sie sicher, dass Sie den Knoten {selectedNode} aus der Organisation entfernen möchten?", + "remoteExitNodeMessageRemove": "Einmal entfernt, wird der Knoten nicht mehr zugänglich sein.", + "remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.", + "remoteExitNodeConfirmDelete": "Löschknoten bestätigen", + "remoteExitNodeDelete": "Knoten löschen", + "sidebarRemoteExitNodes": "Entfernte Knoten", + "remoteExitNodeCreate": { + "title": "Knoten erstellen", + "description": "Erstellen Sie einen neuen Knoten, um Ihre 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.", + "adopt": { + "title": "Node übernehmen", + "description": "Wählen Sie dies, wenn Sie bereits die Anmeldedaten für den Knoten haben." + }, + "generate": { + "title": "Schlüssel generieren", + "description": "Wählen Sie dies, wenn Sie neue Schlüssel für den Knoten generieren möchten" + } + }, + "adopt": { + "title": "Vorhandenen Node übernehmen", + "description": "Geben Sie die Zugangsdaten des vorhandenen Knotens ein, den Sie übernehmen möchten", + "nodeIdLabel": "Knoten-ID", + "nodeIdDescription": "Die ID des vorhandenen Knotens, den Sie übernehmen möchten", + "secretLabel": "Geheimnis", + "secretDescription": "Der geheime Schlüssel des vorhandenen Knotens", + "submitButton": "Node übernehmen" + }, + "generate": { + "title": "Generierte Anmeldedaten", + "description": "Verwenden Sie diese generierten Anmeldeinformationen, um Ihren Knoten zu konfigurieren", + "nodeIdTitle": "Knoten-ID", + "secretTitle": "Geheimnis", + "saveCredentialsTitle": "Anmeldedaten zur Konfiguration hinzufügen", + "saveCredentialsDescription": "Fügen Sie diese Anmeldedaten zu Ihrer selbst-gehosteten Pangolin Node-Konfigurationsdatei hinzu, um die Verbindung abzuschließen.", + "submitButton": "Knoten erstellen" + }, + "validation": { + "adoptRequired": "Knoten-ID und Geheimnis sind erforderlich, wenn ein existierender Knoten angenommen wird" + }, + "errors": { + "loadDefaultsFailed": "Fehler beim Laden der Standardeinstellungen", + "defaultsNotLoaded": "Standardeinstellungen nicht geladen", + "createFailed": "Knoten konnte nicht erstellt werden" + }, + "success": { + "created": "Knoten erfolgreich erstellt" + } + }, + "remoteExitNodeSelection": "Knotenauswahl", + "remoteExitNodeSelectionDescription": "Wählen Sie einen Knoten aus, durch den Traffic für diese lokale Seite geleitet werden soll", + "remoteExitNodeRequired": "Ein Knoten muss für lokale Seiten ausgewählt sein", + "noRemoteExitNodesAvailable": "Keine Knoten verfügbar", + "noRemoteExitNodesAvailableDescription": "Für diese Organisation sind keine Knoten verfügbar. Erstellen Sie zuerst einen Knoten, um lokale Sites zu verwenden.", + "exitNode": "Exit-Node", + "country": "Land", + "rulesMatchCountry": "Derzeit basierend auf der Quell-IP", "managedSelfHosted": { "title": "Verwaltetes Selbsthosted", "description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Internationale Domain erkannt", "willbestoredas": "Wird gespeichert als:", + "roleMappingDescription": "Legen Sie fest, wie den Benutzern Rollen zugewiesen werden, wenn sie sich anmelden, wenn Auto Provision aktiviert ist.", + "selectRole": "Wählen Sie eine Rolle", + "roleMappingExpression": "Ausdruck", + "selectRolePlaceholder": "Rolle auswählen", + "selectRoleDescription": "Wählen Sie eine Rolle aus, die allen Benutzern von diesem Identitätsprovider zugewiesen werden soll", + "roleMappingExpressionDescription": "Geben Sie einen JMESPath-Ausdruck ein, um Rolleninformationen aus dem ID-Token zu extrahieren", + "idpTenantIdRequired": "Mandant ID ist erforderlich", + "invalidValue": "Ungültiger Wert", + "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", + "idpAzureConfiguration": "Azure Entra ID Konfiguration", + "idpAzureConfigurationDescription": "Konfigurieren Sie Ihre Azure Entra ID OAuth2 Zugangsdaten", + "idpTenantId": "Mandanten-ID", + "idpTenantIdPlaceholder": "deine Mandant-ID", + "idpAzureTenantIdDescription": "Ihre Azure Mieter-ID (gefunden in Azure Active Directory Übersicht)", + "idpAzureClientIdDescription": "Ihre Azure App Registration Client ID", + "idpAzureClientSecretDescription": "Ihr Azure App Registration Client Secret", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Google OAuth2/OIDC Provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Eigene Kopfzeilen", - "headersValidationError": "Header müssen im Format Header-Name: Wert sein.", + "subnet": "Subnetz", + "subnetDescription": "Das Subnetz für die Netzwerkkonfiguration dieser Organisation.", + "authPage": "Auth Seite", + "authPageDescription": "Konfigurieren Sie die Auth-Seite für Ihre Organisation", + "authPageDomain": "Domain der Auth Seite", + "noDomainSet": "Keine Domäne gesetzt", + "changeDomain": "Domain ändern", + "selectDomain": "Domain auswählen", + "restartCertificate": "Zertifikat neu starten", + "editAuthPageDomain": "Auth Page Domain bearbeiten", + "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", + "selectDomainForOrgAuthPage": "Wählen Sie eine Domain für die Authentifizierungsseite der Organisation", "domainPickerProvidedDomain": "Angegebene Domain", "domainPickerFreeProvidedDomain": "Kostenlose Domain", "domainPickerVerified": "Verifiziert", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Um diese Funktion nutzen zu können, ist ein Abonnement erforderlich.", + "idpDisabled": "Identitätsanbieter sind deaktiviert.", + "orgAuthPageDisabled": "Organisations-Authentifizierungsseite ist deaktiviert.", + "domainRestartedDescription": "Domain-Verifizierung erfolgreich neu gestartet", "resourceAddEntrypointsEditFile": "Datei bearbeiten: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Datei bearbeiten: docker-compose.yml" + "resourceExposePortsEditFile": "Datei bearbeiten: docker-compose.yml", + "emailVerificationRequired": "E-Mail-Verifizierung ist erforderlich. Bitte melden Sie sich erneut über {dashboardUrl}/auth/login an. Kommen Sie dann wieder hierher.", + "twoFactorSetupRequired": "Die Zwei-Faktor-Authentifizierung ist erforderlich. Bitte melden Sie sich erneut über {dashboardUrl}/auth/login an. Dann kommen Sie hierher zurück.", + "authPageErrorUpdateMessage": "Beim Aktualisieren der Auth-Seiten-Einstellungen ist ein Fehler aufgetreten", + "authPageUpdated": "Auth-Seite erfolgreich aktualisiert", + "healthCheckNotAvailable": "Lokal", + "rewritePath": "Pfad neu schreiben", + "rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird.", + "continueToApplication": "Weiter zur Anwendung", + "checkingInvite": "Einladung wird überprüft", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Header-Auth entfernen", + "resourceHeaderAuthRemoveDescription": "Header-Authentifizierung erfolgreich entfernt.", + "resourceErrorHeaderAuthRemove": "Fehler beim Entfernen der Header-Authentifizierung", + "resourceErrorHeaderAuthRemoveDescription": "Die Headerauthentifizierung für die Ressource konnte nicht entfernt werden.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Fehler beim Setzen der Header-Authentifizierung", + "resourceErrorHeaderAuthSetupDescription": "Konnte Header-Authentifizierung für die Ressource nicht festlegen.", + "resourceHeaderAuthSetup": "Header-Authentifizierung erfolgreich festgelegt", + "resourceHeaderAuthSetupDescription": "Header-Authentifizierung wurde erfolgreich festgelegt.", + "resourceHeaderAuthSetupTitle": "Header-Authentifizierung festlegen", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Header-Authentifizierung festlegen", + "actionSetResourceHeaderAuth": "Header-Authentifizierung festlegen", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priorität", + "priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/en-US.json b/messages/en-US.json index a483eed2..a9f78517 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -94,9 +94,9 @@ "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", "siteWg": "Basic WireGuard", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", - "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", + "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", "siteLocalDescription": "Local resources only. No tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "See All Sites", "siteTunnelDescription": "Determine how you want to connect to your site", "siteNewtCredentials": "Newt Credentials", @@ -159,7 +159,7 @@ "resourceHTTP": "HTTPS Resource", "resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.", "resourceRaw": "Raw TCP/UDP Resource", - "resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.", + "resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number. This only works when sites are connected to nodes.", "resourceCreate": "Create Resource", "resourceCreateDescription": "Follow the steps below to create a new resource", "resourceSeeAll": "See All Resources", @@ -168,6 +168,9 @@ "siteSelect": "Select site", "siteSearch": "Search site", "siteNotFound": "No site found.", + "selectCountry": "Select country", + "searchCountries": "Search countries...", + "noCountryFound": "No country found.", "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Resource Type", "resourceTypeDescription": "Determine how you want to access your resource", @@ -465,7 +468,10 @@ "createdAt": "Created At", "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - "proxyEnableSSL": "Enable SSL (https)", + "proxyEnableSSL": "Enable SSL", + "proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.", + "target": "Target", + "configureTarget": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", "targetErrorFetchDescription": "An error occurred while fetching targets", "siteErrorFetch": "Failed to fetch resource", @@ -492,7 +498,7 @@ "targetTlsSettings": "Secure Connection Configuration", "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", "targetTlsSettingsAdvanced": "Advanced TLS Settings", - "targetTlsSni": "TLS Server Name (SNI)", + "targetTlsSni": "TLS Server Name", "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", "targetSubmit": "Add Target", - "targetNoOne": "No targets. Add a target using the form.", + "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.", "targetsSubmit": "Save Targets", + "addTarget": "Add Target", + "targetErrorInvalidIp": "Invalid IP address", + "targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname", + "targetErrorInvalidPort": "Invalid port", + "targetErrorInvalidPortDescription": "Please enter a valid port number", + "targetErrorNoSite": "No site selected", + "targetErrorNoSiteDescription": "Please select a site for the target", + "targetCreated": "Target created", + "targetCreatedDescription": "Target has been created successfully", + "targetErrorCreate": "Failed to create target", + "targetErrorCreateDescription": "An error occurred while creating the target", + "save": "Save", "proxyAdditional": "Additional Proxy Settings", "proxyAdditionalDescription": "Configure how your resource handles proxy settings", "proxyCustomHeader": "Custom Host Header", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Server Admin - Pangolin", "licenseTierProfessional": "Professional License", "licenseTierEnterprise": "Enterprise License", - "licenseTierCommercial": "Commercial License", + "licenseTierPersonal": "Personal License", "licensed": "Licensed", "yes": "Yes", "no": "No", @@ -747,7 +765,7 @@ "idpDisplayName": "A display name for this identity provider", "idpAutoProvisionUsers": "Auto Provision Users", "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", - "licenseBadge": "Professional", + "licenseBadge": "EE", "idpType": "Provider Type", "idpTypeDescription": "Select the type of identity provider you want to configure", "idpOidcConfigure": "OAuth2/OIDC Configuration", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Connected", "idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.", "idpErrorNotFound": "IdP not found", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Invalid Invite", "inviteInvalidDescription": "The invite link is invalid.", "inviteErrorWrongUser": "Invite is not for this user", @@ -1083,7 +1099,6 @@ "navbar": "Navigation Menu", "navbarDescription": "Main navigation menu for the application", "navbarDocsLink": "Documentation", - "commercialEdition": "Commercial Edition", "otpErrorEnable": "Unable to enable 2FA", "otpErrorEnableDescription": "An error occurred while enabling 2FA", "otpSetupCheckCode": "Please enter a 6-digit code", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", "sidebarLicense": "License", - "sidebarClients": "Clients (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domains", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Subdomain: {subdomain}", "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Show More", + "regionSelectorTitle": "Select Region", + "regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.", + "regionSelectorPlaceholder": "Choose a region", + "regionSelectorComingSoon": "Coming Soon", + "billingLoadingSubscription": "Loading subscription...", + "billingFreeTier": "Free Tier", + "billingWarningOverLimit": "Warning: You have exceeded one or more usage limits. Your sites will not connect until you modify your subscription or adjust your usage.", + "billingUsageLimitsOverview": "Usage Limits Overview", + "billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@fossorial.io.", + "billingDataUsage": "Data Usage", + "billingOnlineTime": "Site Online Time", + "billingUsers": "Active Users", + "billingDomains": "Active Domains", + "billingRemoteExitNodes": "Active Self-hosted Nodes", + "billingNoLimitConfigured": "No limit configured", + "billingEstimatedPeriod": "Estimated Billing Period", + "billingIncludedUsage": "Included Usage", + "billingIncludedUsageDescription": "Usage included with your current subscription plan", + "billingFreeTierIncludedUsage": "Free tier usage allowances", + "billingIncluded": "included", + "billingEstimatedTotal": "Estimated Total:", + "billingNotes": "Notes", + "billingEstimateNote": "This is an estimate based on your current usage.", + "billingActualChargesMayVary": "Actual charges may vary.", + "billingBilledAtEnd": "You will be billed at the end of the billing period.", + "billingModifySubscription": "Modify Subscription", + "billingStartSubscription": "Start Subscription", + "billingRecurringCharge": "Recurring Charge", + "billingManageSubscriptionSettings": "Manage your subscription settings and preferences", + "billingNoActiveSubscription": "You don't have an active subscription. Start your subscription to increase usage limits.", + "billingFailedToLoadSubscription": "Failed to load subscription", + "billingFailedToLoadUsage": "Failed to load usage", + "billingFailedToGetCheckoutUrl": "Failed to get checkout URL", + "billingPleaseTryAgainLater": "Please try again later.", + "billingCheckoutError": "Checkout Error", + "billingFailedToGetPortalUrl": "Failed to get portal URL", + "billingPortalError": "Portal Error", + "billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.", + "billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.", + "billingUsersInfo": "You're charged for each user in your organization. Billing is calculated daily based on the number of active user accounts in your org.", + "billingDomainInfo": "You're charged for each domain in your organization. Billing is calculated daily based on the number of active domain accounts in your org.", + "billingRemoteExitNodesInfo": "You're charged for each managed Node in your organization. Billing is calculated daily based on the number of active managed Nodes in your org.", "domainNotFound": "Domain Not Found", "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", "failed": "Failed", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Two-factor authentication is required to register a security key.", "twoFactor": "Two-Factor Authentication", "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", - "continueToApplication": "Continue to Application", "securityKeyAdd": "Add Security Key", "securityKeyRegisterTitle": "Register New Security Key", "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", "resourcePortRequired": "Port number is required for non-HTTP resources", "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "billingPricingCalculatorLink": "Pricing Calculator", "signUpTerms": { "IAgreeToThe": "I agree to the", "termsOfService": "terms of service", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "External Proxy Enabled", "addNewTarget": "Add New Target", "targetsList": "Targets List", + "advancedMode": "Advanced Mode", "targetErrorDuplicateTargetFound": "Duplicate target found", + "healthCheckHealthy": "Healthy", + "healthCheckUnhealthy": "Unhealthy", + "healthCheckUnknown": "Unknown", + "healthCheck": "Health Check", + "configureHealthCheck": "Configure Health Check", + "configureHealthCheckDescription": "Set up health monitoring for {target}", + "enableHealthChecks": "Enable Health Checks", + "enableHealthChecksDescription": "Monitor the health of this target. You can monitor a different endpoint than the target if required.", + "healthScheme": "Method", + "healthSelectScheme": "Select Method", + "healthCheckPath": "Path", + "healthHostname": "IP / Host", + "healthPort": "Port", + "healthCheckPathDescription": "The path to check for health status.", + "healthyIntervalSeconds": "Healthy Interval", + "unhealthyIntervalSeconds": "Unhealthy Interval", + "IntervalSeconds": "Healthy Interval", + "timeoutSeconds": "Timeout", + "timeIsInSeconds": "Time is in seconds", + "retryAttempts": "Retry Attempts", + "expectedResponseCodes": "Expected Response Codes", + "expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.", + "customHeaders": "Custom Headers", + "customHeadersDescription": "Headers new line separated: Header-Name: value", + "headersValidationError": "Headers must be in the format: Header-Name: value", + "saveHealthCheck": "Save Health Check", + "healthCheckSaved": "Health Check Saved", + "healthCheckSavedDescription": "Health check configuration has been saved successfully", + "healthCheckError": "Health Check Error", + "healthCheckErrorDescription": "An error occurred while saving the health check configuration", + "healthCheckPathRequired": "Health check path is required", + "healthCheckMethodRequired": "HTTP method is required", + "healthCheckIntervalMin": "Check interval must be at least 5 seconds", + "healthCheckTimeoutMin": "Timeout must be at least 1 second", + "healthCheckRetryMin": "Retry attempts must be at least 1", "httpMethod": "HTTP Method", "selectHttpMethod": "Select HTTP method", "domainPickerSubdomainLabel": "Subdomain", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", "domainPickerFreeDomains": "Free Domains", "domainPickerSearchForAvailableDomains": "Search for available domains", + "domainPickerNotWorkSelfHosted": "Note: Free provided domains are not available for self-hosted instances right now.", "resourceDomain": "Domain", "resourceEditDomain": "Edit Domain", "siteName": "Site Name", @@ -1463,6 +1557,72 @@ "autoLoginError": "Auto Login Error", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", + "remoteExitNodeManageRemoteExitNodes": "Remote Nodes", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nodes", + "searchRemoteExitNodes": "Search nodes...", + "remoteExitNodeAdd": "Add Node", + "remoteExitNodeErrorDelete": "Error deleting node", + "remoteExitNodeQuestionRemove": "Are you sure you want to remove the node {selectedNode} from the organization?", + "remoteExitNodeMessageRemove": "Once removed, the node will no longer be accessible.", + "remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.", + "remoteExitNodeConfirmDelete": "Confirm Delete Node", + "remoteExitNodeDelete": "Delete Node", + "sidebarRemoteExitNodes": "Remote Nodes", + "remoteExitNodeCreate": { + "title": "Create Node", + "description": "Create a new node to extend your network connectivity", + "viewAllButton": "View All Nodes", + "strategy": { + "title": "Creation Strategy", + "description": "Choose this to manually configure your node or generate new credentials.", + "adopt": { + "title": "Adopt Node", + "description": "Choose this if you already have the credentials for the node." + }, + "generate": { + "title": "Generate Keys", + "description": "Choose this if you want to generate new keys for the node" + } + }, + "adopt": { + "title": "Adopt Existing Node", + "description": "Enter the credentials of the existing node you want to adopt", + "nodeIdLabel": "Node ID", + "nodeIdDescription": "The ID of the existing node you want to adopt", + "secretLabel": "Secret", + "secretDescription": "The secret key of the existing node", + "submitButton": "Adopt Node" + }, + "generate": { + "title": "Generated Credentials", + "description": "Use these generated credentials to configure your node", + "nodeIdTitle": "Node ID", + "secretTitle": "Secret", + "saveCredentialsTitle": "Add Credentials to Config", + "saveCredentialsDescription": "Add these credentials to your self-hosted Pangolin node configuration file to complete the connection.", + "submitButton": "Create Node" + }, + "validation": { + "adoptRequired": "Node ID and Secret are required when adopting an existing node" + }, + "errors": { + "loadDefaultsFailed": "Failed to load defaults", + "defaultsNotLoaded": "Defaults not loaded", + "createFailed": "Failed to create node" + }, + "success": { + "created": "Node created successfully" + } + }, + "remoteExitNodeSelection": "Node Selection", + "remoteExitNodeSelectionDescription": "Select a node to route traffic through for this local site", + "remoteExitNodeRequired": "A node must be selected for local sites", + "noRemoteExitNodesAvailable": "No Nodes Available", + "noRemoteExitNodesAvailableDescription": "No nodes are available for this organization. Create a node first to use local sites.", + "exitNode": "Exit Node", + "country": "Country", + "rulesMatchCountry": "Currently based on source IP", "managedSelfHosted": { "title": "Managed Self-Hosted", "description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "International Domain Detected", "willbestoredas": "Will be stored as:", + "roleMappingDescription": "Determine how roles are assigned to users when they sign in when Auto Provision is enabled.", + "selectRole": "Select a Role", + "roleMappingExpression": "Expression", + "selectRolePlaceholder": "Choose a role", + "selectRoleDescription": "Select a role to assign to all users from this identity provider", + "roleMappingExpressionDescription": "Enter a JMESPath expression to extract role information from the ID token", + "idpTenantIdRequired": "Tenant ID is required", + "invalidValue": "Invalid value", + "idpTypeLabel": "Identity Provider Type", + "roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'", + "idpGoogleConfiguration": "Google Configuration", + "idpGoogleConfigurationDescription": "Configure your Google OAuth2 credentials", + "idpGoogleClientIdDescription": "Your Google OAuth2 Client ID", + "idpGoogleClientSecretDescription": "Your Google OAuth2 Client Secret", + "idpAzureConfiguration": "Azure Entra ID Configuration", + "idpAzureConfigurationDescription": "Configure your Azure Entra ID OAuth2 credentials", + "idpTenantId": "Tenant ID", + "idpTenantIdPlaceholder": "your-tenant-id", + "idpAzureTenantIdDescription": "Your Azure tenant ID (found in Azure Active Directory overview)", + "idpAzureClientIdDescription": "Your Azure App Registration Client ID", + "idpAzureClientSecretDescription": "Your Azure App Registration Client Secret", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Google Configuration", + "idpAzureConfigurationTitle": "Azure Entra ID Configuration", + "idpTenantIdLabel": "Tenant ID", + "idpAzureClientIdDescription2": "Your Azure App Registration Client ID", + "idpAzureClientSecretDescription2": "Your Azure App Registration Client Secret", "idpGoogleDescription": "Google OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Custom Headers", - "headersValidationError": "Headers must be in the format: Header-Name: value.", + "subnet": "Subnet", + "subnetDescription": "The subnet for this organization's network configuration.", + "authPage": "Auth Page", + "authPageDescription": "Configure the auth page for your organization", + "authPageDomain": "Auth Page Domain", + "noDomainSet": "No domain set", + "changeDomain": "Change Domain", + "selectDomain": "Select Domain", + "restartCertificate": "Restart Certificate", + "editAuthPageDomain": "Edit Auth Page Domain", + "setAuthPageDomain": "Set Auth Page Domain", + "failedToFetchCertificate": "Failed to fetch certificate", + "failedToRestartCertificate": "Failed to restart certificate", + "addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for your organization", + "selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page", "domainPickerProvidedDomain": "Provided Domain", "domainPickerFreeProvidedDomain": "Free Provided Domain", "domainPickerVerified": "Verified", @@ -1518,6 +1721,178 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", "domainPickerSubdomainSanitized": "Subdomain sanitized", "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", + "orgAuthSignInTitle": "Sign in to your organization", + "orgAuthChooseIdpDescription": "Choose your identity provider to continue", + "orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.", + "orgAuthSignInWithPangolin": "Sign in with Pangolin", + "subscriptionRequiredToUse": "A subscription is required to use this feature.", + "idpDisabled": "Identity providers are disabled.", + "orgAuthPageDisabled": "Organization auth page is disabled.", + "domainRestartedDescription": "Domain verification restarted successfully", "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Edit file: docker-compose.yml" + "resourceExposePortsEditFile": "Edit file: docker-compose.yml", + "emailVerificationRequired": "Email verification is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here.", + "twoFactorSetupRequired": "Two-factor authentication setup is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here.", + "authPageErrorUpdateMessage": "An error occurred while updating the auth page settings", + "authPageUpdated": "Auth page updated successfully", + "healthCheckNotAvailable": "Local", + "rewritePath": "Rewrite Path", + "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", + "continueToApplication": "Continue to application", + "checkingInvite": "Checking Invite", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Remove Header Auth", + "resourceHeaderAuthRemoveDescription": "Header authentication removed successfully.", + "resourceErrorHeaderAuthRemove": "Failed to remove Header Authentication", + "resourceErrorHeaderAuthRemoveDescription": "Could not remove header authentication for the resource.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Failed to set Header Authentication", + "resourceErrorHeaderAuthSetupDescription": "Could not set header authentication for the resource.", + "resourceHeaderAuthSetup": "Header Authentication set successfully", + "resourceHeaderAuthSetupDescription": "Header authentication has been successfully set.", + "resourceHeaderAuthSetupTitle": "Set Header Authentication", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Set Header Authentication", + "actionSetResourceHeaderAuth": "Set Header Authentication", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that the information I provided is accurate and that I am in compliance with the Fossorial Commercial License. Reporting inaccurate information or misidentifying use of the product is a violation of the license and may result in your key getting revoked." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priority", + "priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip", + "sidebarEnableEnterpriseLicense": "Enable Enterprise License" } diff --git a/messages/es-ES.json b/messages/es-ES.json index 0a835b33..17f9ad44 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -67,7 +67,7 @@ "siteDocker": "Expandir para detalles de despliegue de Docker", "toggle": "Cambiar", "dockerCompose": "Componer Docker", - "dockerRun": "Docker Run", + "dockerRun": "Ejecutar Docker", "siteLearnLocal": "Los sitios locales no tienen túnel, aprender más", "siteConfirmCopy": "He copiado la configuración", "searchSitesProgress": "Buscar sitios...", @@ -96,7 +96,7 @@ "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. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Ver todos los sitios", "siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio", "siteNewtCredentials": "Credenciales nuevas", @@ -168,6 +168,9 @@ "siteSelect": "Seleccionar sitio", "siteSearch": "Buscar sitio", "siteNotFound": "Sitio no encontrado.", + "selectCountry": "Seleccionar país", + "searchCountries": "Buscar países...", + "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", @@ -465,7 +468,10 @@ "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": "Habilitar SSL (https)", + "proxyEnableSSL": "Activar SSL", + "proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.", + "target": "Target", + "configureTarget": "Configurar objetivos", "targetErrorFetch": "Error al recuperar los objetivos", "targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos", "siteErrorFetch": "No se pudo obtener el recurso", @@ -492,7 +498,7 @@ "targetTlsSettings": "Configuración de conexión segura", "targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso", "targetTlsSettingsAdvanced": "Ajustes avanzados de TLS", - "targetTlsSni": "Nombre del servidor TLS (SNI)", + "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", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", "methodSelect": "Seleccionar método", "targetSubmit": "Añadir destino", - "targetNoOne": "No hay objetivos. Agregue un objetivo usando el formulario.", + "targetNoOne": "Este recurso no tiene ningún objetivo. Agrega un objetivo para configurar dónde enviar peticiones al backend.", "targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.", "targetsSubmit": "Guardar objetivos", + "addTarget": "Añadir destino", + "targetErrorInvalidIp": "Dirección IP inválida", + "targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host", + "targetErrorInvalidPort": "Puerto inválido", + "targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido", + "targetErrorNoSite": "Ningún sitio seleccionado", + "targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo", + "targetCreated": "Objetivo creado", + "targetCreatedDescription": "El objetivo se ha creado correctamente", + "targetErrorCreate": "Error al crear el objetivo", + "targetErrorCreateDescription": "Se ha producido un error al crear el objetivo", + "save": "Guardar", "proxyAdditional": "Ajustes adicionales del proxy", "proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy", "proxyCustomHeader": "Cabecera de host personalizada", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Admin Servidor - Pangolin", "licenseTierProfessional": "Licencia profesional", "licenseTierEnterprise": "Licencia Enterprise", - "licenseTierCommercial": "Licencia comercial", + "licenseTierPersonal": "Personal License", "licensed": "Licenciado", "yes": "Sí", "no": "Nu", @@ -747,7 +765,7 @@ "idpDisplayName": "Un nombre mostrado para este proveedor de identidad", "idpAutoProvisionUsers": "Auto-Provisión de Usuarios", "idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.", - "licenseBadge": "Profesional", + "licenseBadge": "EE", "idpType": "Tipo de proveedor", "idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar", "idpOidcConfigure": "Configuración OAuth2/OIDC", @@ -814,7 +832,7 @@ "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.", - "pangolinAuth": "Auth - Pangolin", + "pangolinAuth": "Autenticación - Pangolin", "verificationCodeLengthRequirements": "Tu código de verificación debe tener 8 caracteres.", "errorOccurred": "Se ha producido un error", "emailErrorVerify": "No se pudo verificar el email:", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Conectado", "idpErrorConnectingTo": "Hubo un problema al conectar con {name}. Por favor, póngase en contacto con su administrador.", "idpErrorNotFound": "IdP no encontrado", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Invitación inválida", "inviteInvalidDescription": "El enlace de invitación no es válido.", "inviteErrorWrongUser": "La invitación no es para este usuario", @@ -1083,7 +1099,6 @@ "navbar": "Menú de navegación", "navbarDescription": "Menú de navegación principal para la aplicación", "navbarDocsLink": "Documentación", - "commercialEdition": "Edición Comercial", "otpErrorEnable": "No se puede habilitar 2FA", "otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA", "otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Todos los usuarios", "sidebarIdentityProviders": "Proveedores de identidad", "sidebarLicense": "Licencia", - "sidebarClients": "Clientes (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Dominios", "enableDockerSocket": "Habilitar Plano Docker", "enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.", @@ -1219,7 +1234,7 @@ "billing": "Facturación", "orgBillingDescription": "Gestiona tu información de facturación y suscripciones", "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", + "pangolinHosted": "Pangolin Alojado", "fossorial": "Fossorial", "completeAccountSetup": "Completar configuración de cuenta", "completeAccountSetupDescription": "Establece tu contraseña para comenzar", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Subdominio: {subdomain}", "domainPickerNamespace": "Espacio de nombres: {namespace}", "domainPickerShowMore": "Mostrar más", + "regionSelectorTitle": "Seleccionar Región", + "regionSelectorInfo": "Seleccionar una región nos ayuda a brindar un mejor rendimiento para tu ubicación. No tienes que estar en la misma región que tu servidor.", + "regionSelectorPlaceholder": "Elige una región", + "regionSelectorComingSoon": "Próximamente", + "billingLoadingSubscription": "Cargando suscripción...", + "billingFreeTier": "Nivel Gratis", + "billingWarningOverLimit": "Advertencia: Has excedido uno o más límites de uso. Tus sitios no se conectarán hasta que modifiques tu suscripción o ajustes tu uso.", + "billingUsageLimitsOverview": "Descripción general de los límites de uso", + "billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@fossorial.io.", + "billingDataUsage": "Uso de datos", + "billingOnlineTime": "Tiempo en línea del sitio", + "billingUsers": "Usuarios activos", + "billingDomains": "Dominios activos", + "billingRemoteExitNodes": "Nodos autogestionados activos", + "billingNoLimitConfigured": "No se ha configurado ningún límite", + "billingEstimatedPeriod": "Período de facturación estimado", + "billingIncludedUsage": "Uso incluido", + "billingIncludedUsageDescription": "Uso incluido con su plan de suscripción actual", + "billingFreeTierIncludedUsage": "Permisos de uso del nivel gratuito", + "billingIncluded": "incluido", + "billingEstimatedTotal": "Total Estimado:", + "billingNotes": "Notas", + "billingEstimateNote": "Esta es una estimación basada en tu uso actual.", + "billingActualChargesMayVary": "Los cargos reales pueden variar.", + "billingBilledAtEnd": "Se te facturará al final del período de facturación.", + "billingModifySubscription": "Modificar Suscripción", + "billingStartSubscription": "Iniciar Suscripción", + "billingRecurringCharge": "Cargo Recurrente", + "billingManageSubscriptionSettings": "Administra la configuración y preferencias de tu 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", + "billingFailedToGetCheckoutUrl": "Error al obtener la URL de pago", + "billingPleaseTryAgainLater": "Por favor, inténtelo de nuevo más tarde.", + "billingCheckoutError": "Error de pago", + "billingFailedToGetPortalUrl": "Error al obtener la URL del portal", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.", "twoFactor": "Autenticación de dos factores", "adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.", - "continueToApplication": "Continuar a la aplicación", "securityKeyAdd": "Agregar llave de seguridad", "securityKeyRegisterTitle": "Registrar nueva llave de seguridad", "securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Los cambios de DNS pueden tardar un tiempo en propagarse a través de internet. Esto puede tardar desde unos pocos minutos hasta 48 horas, dependiendo de tu proveedor de DNS y la configuración de TTL.", "resourcePortRequired": "Se requiere número de puerto para recursos no HTTP", "resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP", + "billingPricingCalculatorLink": "Calculadora de Precios", "signUpTerms": { "IAgreeToThe": "Estoy de acuerdo con los", "termsOfService": "términos del servicio", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Proxy externo habilitado", "addNewTarget": "Agregar nuevo destino", "targetsList": "Lista de destinos", + "advancedMode": "Modo avanzado", "targetErrorDuplicateTargetFound": "Se encontró un destino duplicado", + "healthCheckHealthy": "Saludable", + "healthCheckUnhealthy": "No saludable", + "healthCheckUnknown": "Desconocido", + "healthCheck": "Chequeo de salud", + "configureHealthCheck": "Configurar Chequeo de Salud", + "configureHealthCheckDescription": "Configura la monitorización de salud para {target}", + "enableHealthChecks": "Activar Chequeos de Salud", + "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", + "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", + "IntervalSeconds": "Intervalo Saludable", + "timeoutSeconds": "Tiempo de Espera", + "timeIsInSeconds": "El tiempo está en segundos", + "retryAttempts": "Intentos de Reintento", + "expectedResponseCodes": "Códigos de respuesta esperados", + "expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.", + "customHeaders": "Cabeceras personalizadas", + "customHeadersDescription": "Nueva línea de cabeceras separada: Nombre de cabecera: valor", + "headersValidationError": "Los encabezados deben estar en el formato: Nombre de cabecera: valor.", + "saveHealthCheck": "Guardar Chequeo de Salud", + "healthCheckSaved": "Chequeo de Salud Guardado", + "healthCheckSavedDescription": "La configuración del chequeo de salud se ha guardado correctamente", + "healthCheckError": "Error en el Chequeo de Salud", + "healthCheckErrorDescription": "Ocurrió un error al guardar la configuración del chequeo de salud", + "healthCheckPathRequired": "Se requiere la ruta del chequeo de salud", + "healthCheckMethodRequired": "Se requiere el método HTTP", + "healthCheckIntervalMin": "El intervalo de comprobación debe ser de al menos 5 segundos", + "healthCheckTimeoutMin": "El tiempo de espera debe ser de al menos 1 segundo", + "healthCheckRetryMin": "Los intentos de reintento deben ser de al menos 1", "httpMethod": "Método HTTP", "selectHttpMethod": "Seleccionar método HTTP", "domainPickerSubdomainLabel": "Subdominio", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Ingrese un subdominio para buscar y seleccionar entre dominios gratuitos disponibles.", "domainPickerFreeDomains": "Dominios gratuitos", "domainPickerSearchForAvailableDomains": "Buscar dominios disponibles", + "domainPickerNotWorkSelfHosted": "Nota: Los dominios gratuitos proporcionados no están disponibles para instancias autogestionadas por ahora.", "resourceDomain": "Dominio", "resourceEditDomain": "Editar dominio", "siteName": "Nombre del sitio", @@ -1463,6 +1557,72 @@ "autoLoginError": "Error de inicio de sesión automático", "autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.", "autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.", + "remoteExitNodeManageRemoteExitNodes": "Nodos remotos", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nodos", + "searchRemoteExitNodes": "Buscar nodos...", + "remoteExitNodeAdd": "Añadir Nodo", + "remoteExitNodeErrorDelete": "Error al eliminar el nodo", + "remoteExitNodeQuestionRemove": "¿Está seguro de que desea eliminar el nodo {selectedNode} de la organización?", + "remoteExitNodeMessageRemove": "Una vez eliminado, el nodo ya no será accesible.", + "remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.", + "remoteExitNodeConfirmDelete": "Confirmar eliminar nodo", + "remoteExitNodeDelete": "Eliminar Nodo", + "sidebarRemoteExitNodes": "Nodos remotos", + "remoteExitNodeCreate": { + "title": "Crear Nodo", + "description": "Crear un nuevo nodo para extender la conectividad de red", + "viewAllButton": "Ver todos los nodos", + "strategy": { + "title": "Estrategia de Creación", + "description": "Elija esto para configurar manualmente su nodo o generar nuevas credenciales.", + "adopt": { + "title": "Adoptar Nodo", + "description": "Elija esto si ya tiene las credenciales para el nodo." + }, + "generate": { + "title": "Generar Claves", + "description": "Elija esto si desea generar nuevas claves para el nodo" + } + }, + "adopt": { + "title": "Adoptar Nodo Existente", + "description": "Introduzca las credenciales del nodo existente que desea adoptar", + "nodeIdLabel": "ID del nodo", + "nodeIdDescription": "El ID del nodo existente que desea adoptar", + "secretLabel": "Secreto", + "secretDescription": "La clave secreta del nodo existente", + "submitButton": "Adoptar Nodo" + }, + "generate": { + "title": "Credenciales Generadas", + "description": "Utilice estas credenciales generadas para configurar su nodo", + "nodeIdTitle": "ID del nodo", + "secretTitle": "Secreto", + "saveCredentialsTitle": "Agregar Credenciales a la Configuración", + "saveCredentialsDescription": "Agrega estas credenciales a tu archivo de configuración del nodo Pangolin autogestionado para completar la conexión.", + "submitButton": "Crear Nodo" + }, + "validation": { + "adoptRequired": "El ID del nodo y el secreto son necesarios al adoptar un nodo existente" + }, + "errors": { + "loadDefaultsFailed": "Falló al cargar los valores predeterminados", + "defaultsNotLoaded": "Valores predeterminados no cargados", + "createFailed": "Error al crear el nodo" + }, + "success": { + "created": "Nodo creado correctamente" + } + }, + "remoteExitNodeSelection": "Selección de nodo", + "remoteExitNodeSelectionDescription": "Seleccione un nodo a través del cual enrutar el tráfico para este sitio local", + "remoteExitNodeRequired": "Un nodo debe ser seleccionado para sitios locales", + "noRemoteExitNodesAvailable": "No hay nodos disponibles", + "noRemoteExitNodesAvailableDescription": "No hay nodos disponibles para esta organización. Crea un nodo primero para usar sitios locales.", + "exitNode": "Nodo de Salida", + "country": "País", + "rulesMatchCountry": "Actualmente basado en IP de origen", "managedSelfHosted": { "title": "Autogestionado", "description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Dominio Internacional detectado", "willbestoredas": "Se almacenará como:", + "roleMappingDescription": "Determinar cómo se asignan los roles a los usuarios cuando se registran cuando está habilitada la provisión automática.", + "selectRole": "Seleccione un rol", + "roleMappingExpression": "Expresión", + "selectRolePlaceholder": "Elija un rol", + "selectRoleDescription": "Seleccione un rol para asignar a todos los usuarios de este proveedor de identidad", + "roleMappingExpressionDescription": "Introduzca una expresión JMESPath para extraer información de rol del token de ID", + "idpTenantIdRequired": "El ID del cliente es obligatorio", + "invalidValue": "Valor inválido", + "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", + "idpAzureConfiguration": "Configuración de Azure Entra ID", + "idpAzureConfigurationDescription": "Configure sus 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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Proveedor OAuth2/OIDC de Google", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Cabeceras personalizadas", - "headersValidationError": "Los encabezados deben estar en el formato: Nombre de cabecera: valor.", + "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", + "authPageDomain": "Dominio de la página Auth", + "noDomainSet": "Ningún dominio establecido", + "changeDomain": "Cambiar dominio", + "selectDomain": "Seleccionar dominio", + "restartCertificate": "Reiniciar certificado", + "editAuthPageDomain": "Editar dominio Auth Page", + "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", + "selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización", "domainPickerProvidedDomain": "Dominio proporcionado", "domainPickerFreeProvidedDomain": "Dominio proporcionado gratis", "domainPickerVerified": "Verificado", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Se requiere una suscripción para utilizar esta función.", + "idpDisabled": "Los proveedores de identidad están deshabilitados.", + "orgAuthPageDisabled": "La página de autenticación de la organización está deshabilitada.", + "domainRestartedDescription": "Verificación de dominio reiniciada con éxito", "resourceAddEntrypointsEditFile": "Editar archivo: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Editar archivo: docker-compose.yml" + "resourceExposePortsEditFile": "Editar archivo: docker-compose.yml", + "emailVerificationRequired": "Se requiere verificación de correo electrónico. Por favor, inicie sesión de nuevo a través de {dashboardUrl}/auth/login complete este paso. Luego, vuelva aquí.", + "twoFactorSetupRequired": "La configuración de autenticación de doble factor es requerida. Por favor, inicia sesión de nuevo a través de {dashboardUrl}/auth/login completa este paso. Luego, vuelve aquí.", + "authPageErrorUpdateMessage": "Ocurrió un error mientras se actualizaban los ajustes de la página auth", + "authPageUpdated": "Página auth actualizada correctamente", + "healthCheckNotAvailable": "Local", + "rewritePath": "Reescribir Ruta", + "rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino.", + "continueToApplication": "Continuar a la aplicación", + "checkingInvite": "Comprobando invitación", + "setResourceHeaderAuth": "set-Resource HeaderAuth", + "resourceHeaderAuthRemove": "Eliminar Auth del Encabezado", + "resourceHeaderAuthRemoveDescription": "Autenticación de cabecera eliminada correctamente.", + "resourceErrorHeaderAuthRemove": "Error al eliminar autenticación de cabecera", + "resourceErrorHeaderAuthRemoveDescription": "No se pudo eliminar la autenticación de cabecera del recurso.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Error al establecer autenticación de cabecera", + "resourceErrorHeaderAuthSetupDescription": "No se pudo establecer autenticación de cabecera para el recurso.", + "resourceHeaderAuthSetup": "Autenticación de cabecera establecida correctamente", + "resourceHeaderAuthSetupDescription": "La autenticación de cabecera se ha establecido correctamente.", + "resourceHeaderAuthSetupTitle": "Establecer autenticación de cabecera", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Establecer autenticación de cabecera", + "actionSetResourceHeaderAuth": "Establecer autenticación de cabecera", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Prioridad", + "priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 0918f943..91d69844 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -10,7 +10,7 @@ "setupErrorIdentifier": "L'ID de l'organisation est déjà pris. 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.", - "welcome": "Bienvenue sur Pangolin", + "welcome": "Bienvenue à Pangolin", "welcomeTo": "Bienvenue chez", "componentsCreateOrg": "Créer une organisation", "componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.", @@ -34,13 +34,13 @@ "confirmPassword": "Confirmer le mot de passe", "createAccount": "Créer un compte", "viewSettings": "Afficher les paramètres", - "delete": "Supprimer", + "delete": "Supprimez", "name": "Nom", "online": "En ligne", "offline": "Hors ligne", "site": "Site", - "dataIn": "Données reçues", - "dataOut": "Données envoyées", + "dataIn": "Données dans", + "dataOut": "Données épuisées", "connectionType": "Type de connexion", "tunnelType": "Type de tunnel", "local": "Locale", @@ -96,7 +96,7 @@ "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. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", - "siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Voir tous les sites", "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site", "siteNewtCredentials": "Identifiants Newt", @@ -168,6 +168,9 @@ "siteSelect": "Sélectionner un site", "siteSearch": "Chercher un site", "siteNotFound": "Aucun site trouvé.", + "selectCountry": "Sélectionnez un pays", + "searchCountries": "Recherchez des pays...", + "noCountryFound": "Aucun pays trouvé.", "siteSelectionDescription": "Ce site fournira la connectivité à la cible.", "resourceType": "Type de ressource", "resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource", @@ -175,7 +178,7 @@ "resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS", "domainType": "Type de domaine", "subdomain": "Sous-domaine", - "baseDomain": "Domaine racine", + "baseDomain": "Domaine de base", "subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.", "resourceRawSettings": "Paramètres TCP/UDP", "resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP", @@ -309,7 +312,7 @@ "numberOfSites": "Nombre de sites", "licenseKeySearch": "Rechercher des clés de licence...", "licenseKeyAdd": "Ajouter une clé de licence", - "type": "Type", + "type": "Type de texte", "licenseKeyRequired": "La clé de licence est requise", "licenseTermsAgree": "Vous devez accepter les conditions de licence", "licenseErrorKeyLoad": "Impossible de charger les clés de licence", @@ -465,7 +468,10 @@ "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 (https)", + "proxyEnableSSL": "Activer SSL", + "proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.", + "target": "Target", + "configureTarget": "Configurer les cibles", "targetErrorFetch": "Échec de la récupération des cibles", "targetErrorFetchDescription": "Une erreur s'est produite lors de la récupération des cibles", "siteErrorFetch": "Échec de la récupération de la ressource", @@ -492,7 +498,7 @@ "targetTlsSettings": "Configuration sécurisée de connexion", "targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource", "targetTlsSettingsAdvanced": "Paramètres TLS avancés", - "targetTlsSni": "Nom de serveur TLS (SNI)", + "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", @@ -501,9 +507,21 @@ "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": "Aucune cible. Ajoutez une cible en utilisant le formulaire.", + "targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.", "targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.", "targetsSubmit": "Enregistrer les cibles", + "addTarget": "Ajouter une cible", + "targetErrorInvalidIp": "Adresse IP invalide", + "targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide", + "targetErrorInvalidPort": "Port invalide", + "targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide", + "targetErrorNoSite": "Aucun site sélectionné", + "targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible", + "targetCreated": "Cible créée", + "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", + "save": "Enregistrer", "proxyAdditional": "Paramètres de proxy supplémentaires", "proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy", "proxyCustomHeader": "En-tête Host personnalisé", @@ -598,7 +616,7 @@ "newtId": "ID Newt", "newtSecretKey": "Clé secrète Newt", "architecture": "Architecture", - "sites": "Sites", + "sites": "Espaces", "siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.", "siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard", "siteWgManualConfigurationRequired": "Configuration manuelle requise", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Admin Serveur - Pangolin", "licenseTierProfessional": "Licence Professionnelle", "licenseTierEnterprise": "Licence Entreprise", - "licenseTierCommercial": "Licence commerciale", + "licenseTierPersonal": "Personal License", "licensed": "Sous licence", "yes": "Oui", "no": "Non", @@ -747,7 +765,7 @@ "idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité", "idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs", "idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.", - "licenseBadge": "Professionnel", + "licenseBadge": "EE", "idpType": "Type de fournisseur", "idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer", "idpOidcConfigure": "Configuration OAuth2/OIDC", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Connecté", "idpErrorConnectingTo": "Un problème est survenu lors de la connexion à {name}. Veuillez contacter votre administrateur.", "idpErrorNotFound": "IdP introuvable", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Invitation invalide", "inviteInvalidDescription": "Le lien d'invitation n'est pas valide.", "inviteErrorWrongUser": "L'invitation n'est pas pour cet utilisateur", @@ -1083,7 +1099,6 @@ "navbar": "Menu de navigation", "navbarDescription": "Menu de navigation principal de l'application", "navbarDocsLink": "Documentation", - "commercialEdition": "Édition Commerciale", "otpErrorEnable": "Impossible d'activer l'A2F", "otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F", "otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres", @@ -1128,7 +1143,7 @@ "sidebarOverview": "Aperçu", "sidebarHome": "Domicile", "sidebarSites": "Espaces", - "sidebarResources": "Ressources", + "sidebarResources": "Ressource", "sidebarAccessControl": "Contrôle d'accès", "sidebarUsers": "Utilisateurs", "sidebarInvitations": "Invitations", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Tous les utilisateurs", "sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarLicense": "Licence", - "sidebarClients": "Clients (Bêta)", + "sidebarClients": "Clients", "sidebarDomains": "Domaines", "enableDockerSocket": "Activer le Plan Docker", "enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Sous-domaine : {subdomain}", "domainPickerNamespace": "Espace de noms : {namespace}", "domainPickerShowMore": "Afficher plus", + "regionSelectorTitle": "Sélectionner Région", + "regionSelectorInfo": "Sélectionner une région nous aide à offrir de meilleures performances pour votre localisation. Vous n'avez pas besoin d'être dans la même région que votre serveur.", + "regionSelectorPlaceholder": "Choisissez une région", + "regionSelectorComingSoon": "Bientôt disponible", + "billingLoadingSubscription": "Chargement de l'abonnement...", + "billingFreeTier": "Niveau gratuit", + "billingWarningOverLimit": "Attention : Vous avez dépassé une ou plusieurs limites d'utilisation. Vos sites ne se connecteront pas tant que vous n'avez pas modifié votre abonnement ou ajusté votre utilisation.", + "billingUsageLimitsOverview": "Vue d'ensemble des limites d'utilisation", + "billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@fossorial.io.", + "billingDataUsage": "Utilisation des données", + "billingOnlineTime": "Temps en ligne du site", + "billingUsers": "Utilisateurs actifs", + "billingDomains": "Domaines actifs", + "billingRemoteExitNodes": "Nœuds auto-hébergés actifs", + "billingNoLimitConfigured": "Aucune limite configurée", + "billingEstimatedPeriod": "Période de facturation estimée", + "billingIncludedUsage": "Utilisation incluse", + "billingIncludedUsageDescription": "Utilisation incluse dans votre plan d'abonnement actuel", + "billingFreeTierIncludedUsage": "Tolérances d'utilisation du niveau gratuit", + "billingIncluded": "inclus", + "billingEstimatedTotal": "Total estimé :", + "billingNotes": "Notes", + "billingEstimateNote": "Ceci est une estimation basée sur votre utilisation actuelle.", + "billingActualChargesMayVary": "Les frais réels peuvent varier.", + "billingBilledAtEnd": "Vous serez facturé à la fin de la période de facturation.", + "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", + "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", + "billingFailedToGetCheckoutUrl": "Échec pour obtenir l'URL de paiement", + "billingPleaseTryAgainLater": "Veuillez réessayer plus tard.", + "billingCheckoutError": "Erreur de paiement", + "billingFailedToGetPortalUrl": "Échec pour obtenir l'URL du portail", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.", "twoFactor": "Authentification à deux facteurs", "adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.", - "continueToApplication": "Continuer vers l'application", "securityKeyAdd": "Ajouter une clé de sécurité", "securityKeyRegisterTitle": "Enregistrer une nouvelle clé de sécurité", "securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.", "resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP", "resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP", + "billingPricingCalculatorLink": "Calculateur de prix", "signUpTerms": { "IAgreeToThe": "Je suis d'accord avec", "termsOfService": "les conditions d'utilisation", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Proxy externe activé", "addNewTarget": "Ajouter une nouvelle cible", "targetsList": "Liste des cibles", + "advancedMode": "Mode Avancé", "targetErrorDuplicateTargetFound": "Cible en double trouvée", + "healthCheckHealthy": "Sain", + "healthCheckUnhealthy": "En mauvaise santé", + "healthCheckUnknown": "Inconnu", + "healthCheck": "Vérification de l'état de santé", + "configureHealthCheck": "Configurer la vérification de l'état de santé", + "configureHealthCheckDescription": "Configurer la surveillance de la santé pour {target}", + "enableHealthChecks": "Activer les vérifications de santé", + "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", + "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é", + "IntervalSeconds": "Intervalle sain", + "timeoutSeconds": "Délai", + "timeIsInSeconds": "Le temps est exprimé en secondes", + "retryAttempts": "Tentatives de réessai", + "expectedResponseCodes": "Codes de réponse attendus", + "expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.", + "customHeaders": "En-têtes personnalisés", + "customHeadersDescription": "En-têtes séparés par une nouvelle ligne: En-nom: valeur", + "headersValidationError": "Les entêtes doivent être au format : Header-Name: valeur.", + "saveHealthCheck": "Sauvegarder la vérification de l'état de santé", + "healthCheckSaved": "Vérification de l'état de santé enregistrée", + "healthCheckSavedDescription": "La configuration de la vérification de l'état de santé a été enregistrée avec succès", + "healthCheckError": "Erreur de vérification de l'état de santé", + "healthCheckErrorDescription": "Une erreur s'est produite lors de l'enregistrement de la configuration de la vérification de l'état de santé", + "healthCheckPathRequired": "Le chemin de vérification de l'état de santé est requis", + "healthCheckMethodRequired": "La méthode HTTP est requise", + "healthCheckIntervalMin": "L'intervalle de vérification doit être d'au moins 5 secondes", + "healthCheckTimeoutMin": "Le délai doit être d'au moins 1 seconde", + "healthCheckRetryMin": "Les tentatives de réessai doivent être d'au moins 1", "httpMethod": "Méthode HTTP", "selectHttpMethod": "Sélectionnez la méthode HTTP", "domainPickerSubdomainLabel": "Sous-domaine", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Entrez un sous-domaine pour rechercher et sélectionner parmi les domaines gratuits disponibles.", "domainPickerFreeDomains": "Domaines gratuits", "domainPickerSearchForAvailableDomains": "Rechercher des domaines disponibles", + "domainPickerNotWorkSelfHosted": "Remarque : Les domaines fournis gratuitement ne sont pas disponibles pour les instances auto-hébergées pour le moment.", "resourceDomain": "Domaine", "resourceEditDomain": "Modifier le domaine", "siteName": "Nom du site", @@ -1463,6 +1557,72 @@ "autoLoginError": "Erreur de connexion automatique", "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.", + "remoteExitNodeManageRemoteExitNodes": "Nœuds distants", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nœuds", + "searchRemoteExitNodes": "Rechercher des nœuds...", + "remoteExitNodeAdd": "Ajouter un noeud", + "remoteExitNodeErrorDelete": "Erreur lors de la suppression du noeud", + "remoteExitNodeQuestionRemove": "Êtes-vous sûr de vouloir supprimer le noeud {selectedNode} de l'organisation ?", + "remoteExitNodeMessageRemove": "Une fois supprimé, le noeud ne sera plus accessible.", + "remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.", + "remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud", + "remoteExitNodeDelete": "Supprimer le noeud", + "sidebarRemoteExitNodes": "Nœuds distants", + "remoteExitNodeCreate": { + "title": "Créer un noeud", + "description": "Créer un nouveau nœud pour étendre votre 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.", + "adopt": { + "title": "Adopter un nœud", + "description": "Choisissez ceci si vous avez déjà les identifiants pour le noeud." + }, + "generate": { + "title": "Générer des clés", + "description": "Choisissez ceci si vous voulez générer de nouvelles clés pour le noeud" + } + }, + "adopt": { + "title": "Adopter un nœud existant", + "description": "Entrez les identifiants du noeud existant que vous souhaitez adopter", + "nodeIdLabel": "Nœud ID", + "nodeIdDescription": "L'ID du noeud existant que vous voulez adopter", + "secretLabel": "Secret", + "secretDescription": "La clé secrète du noeud existant", + "submitButton": "Noeud d'Adopt" + }, + "generate": { + "title": "Informations d'identification générées", + "description": "Utilisez ces identifiants générés pour configurer votre noeud", + "nodeIdTitle": "Nœud ID", + "secretTitle": "Secret", + "saveCredentialsTitle": "Ajouter des identifiants à la config", + "saveCredentialsDescription": "Ajoutez ces informations d'identification à votre fichier de configuration du nœud Pangolin auto-hébergé pour compléter la connexion.", + "submitButton": "Créer un noeud" + }, + "validation": { + "adoptRequired": "ID de nœud et secret sont requis lors de l'adoption d'un noeud existant" + }, + "errors": { + "loadDefaultsFailed": "Échec du chargement des valeurs par défaut", + "defaultsNotLoaded": "Valeurs par défaut non chargées", + "createFailed": "Impossible de créer le noeud" + }, + "success": { + "created": "Noeud créé avec succès" + } + }, + "remoteExitNodeSelection": "Sélection du noeud", + "remoteExitNodeSelectionDescription": "Sélectionnez un nœud pour acheminer le trafic pour ce site local", + "remoteExitNodeRequired": "Un noeud doit être sélectionné pour les sites locaux", + "noRemoteExitNodesAvailable": "Aucun noeud disponible", + "noRemoteExitNodesAvailableDescription": "Aucun noeud n'est disponible pour cette organisation. Créez d'abord un noeud pour utiliser des sites locaux.", + "exitNode": "Nœud de sortie", + "country": "Pays", + "rulesMatchCountry": "Actuellement basé sur l'IP source", "managedSelfHosted": { "title": "Gestion autonome", "description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Domaine international détecté", "willbestoredas": "Sera stocké comme :", + "roleMappingDescription": "Détermine comment les rôles sont assignés aux utilisateurs lorsqu'ils se connectent lorsque la fourniture automatique est activée.", + "selectRole": "Sélectionnez un rôle", + "roleMappingExpression": "Expression", + "selectRolePlaceholder": "Choisir un rôle", + "selectRoleDescription": "Sélectionnez un rôle à assigner à tous les utilisateurs de ce fournisseur d'identité", + "roleMappingExpressionDescription": "Entrez une expression JMESPath pour extraire les informations du rôle du jeton ID", + "idpTenantIdRequired": "L'ID du locataire est requis", + "invalidValue": "Valeur non valide", + "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", + "idpAzureConfiguration": "Configuration de l'entra ID Azure", + "idpAzureConfigurationDescription": "Configurer vos identifiants OAuth2 Azure Entra", + "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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Fournisseur Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "En-têtes personnalisés", - "headersValidationError": "Les entêtes doivent être au format : Header-Name: valeur.", + "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", + "authPageDomain": "Domaine de la page d'authentification", + "noDomainSet": "Aucun domaine défini", + "changeDomain": "Changer de domaine", + "selectDomain": "Sélectionner un domaine", + "restartCertificate": "Redémarrer le certificat", + "editAuthPageDomain": "Modifier le domaine de la page d'authentification", + "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", + "selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation", "domainPickerProvidedDomain": "Domaine fourni", "domainPickerFreeProvidedDomain": "Domaine fourni gratuitement", "domainPickerVerified": "Vérifié", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Un abonnement est requis pour utiliser cette fonctionnalité.", + "idpDisabled": "Les fournisseurs d'identité sont désactivés.", + "orgAuthPageDisabled": "La page d'authentification de l'organisation est désactivée.", + "domainRestartedDescription": "La vérification du domaine a été redémarrée avec succès", "resourceAddEntrypointsEditFile": "Modifier le fichier : config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Modifier le fichier : docker-compose.yml" + "resourceExposePortsEditFile": "Modifier le fichier : docker-compose.yml", + "emailVerificationRequired": "La vérification de l'e-mail est requise. Veuillez vous reconnecter via {dashboardUrl}/auth/login terminé cette étape. Puis revenez ici.", + "twoFactorSetupRequired": "La configuration d'authentification à deux facteurs est requise. Veuillez vous reconnecter via {dashboardUrl}/auth/login terminé cette étape. Puis revenez ici.", + "authPageErrorUpdateMessage": "Une erreur s'est produite lors de la mise à jour de la page d\u000027authentification", + "authPageUpdated": "Page d\u000027authentification mise à jour avec succès", + "healthCheckNotAvailable": "Locale", + "rewritePath": "Réécrire le chemin", + "rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible.", + "continueToApplication": "Continuer vers l'application", + "checkingInvite": "Vérification de l'invitation", + "setResourceHeaderAuth": "Définir l\\'authentification d\\'en-tête de la ressource", + "resourceHeaderAuthRemove": "Supprimer l'authentification de l'en-tête", + "resourceHeaderAuthRemoveDescription": "Authentification de l'en-tête supprimée avec succès.", + "resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification de l'en-tête", + "resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification de l'en-tête de la ressource.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification de l'en-tête", + "resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification de l'en-tête pour la ressource.", + "resourceHeaderAuthSetup": "Authentification de l'en-tête définie avec succès", + "resourceHeaderAuthSetupDescription": "L'authentification de l'en-tête a été définie avec succès.", + "resourceHeaderAuthSetupTitle": "Authentification de l'en-tête", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Authentification de l'en-tête", + "actionSetResourceHeaderAuth": "Authentification de l'en-tête", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priorité", + "priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/it-IT.json b/messages/it-IT.json index f0a862cd..8031f60e 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -36,8 +36,8 @@ "viewSettings": "Visualizza impostazioni", "delete": "Elimina", "name": "Nome", - "online": "Online", - "offline": "Offline", + "online": "In linea", + "offline": "Non in linea", "site": "Sito", "dataIn": "Dati In", "dataOut": "Dati Fuori", @@ -96,7 +96,7 @@ "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. FUNZIONA SOLO SU NODI AUTO-OSPITATI", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Vedi Tutti I Siti", "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", "siteNewtCredentials": "Credenziali Newt", @@ -168,6 +168,9 @@ "siteSelect": "Seleziona sito", "siteSearch": "Cerca sito", "siteNotFound": "Nessun sito trovato.", + "selectCountry": "Seleziona paese", + "searchCountries": "Cerca paesi...", + "noCountryFound": "Nessun paese trovato.", "siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.", "resourceType": "Tipo Di Risorsa", "resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa", @@ -465,7 +468,10 @@ "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 (https)", + "proxyEnableSSL": "Abilita SSL", + "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.", + "target": "Target", + "configureTarget": "Configura Obiettivi", "targetErrorFetch": "Impossibile recuperare i target", "targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target", "siteErrorFetch": "Impossibile recuperare la risorsa", @@ -492,7 +498,7 @@ "targetTlsSettings": "Configurazione Connessione Sicura", "targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa", "targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate", - "targetTlsSni": "Nome Server TLS (SNI)", + "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", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", "methodSelect": "Seleziona metodo", "targetSubmit": "Aggiungi Target", - "targetNoOne": "Nessun target. Aggiungi un target usando il modulo.", + "targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.", "targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.", "targetsSubmit": "Salva Target", + "addTarget": "Aggiungi Target", + "targetErrorInvalidIp": "Indirizzo IP non valido", + "targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido", + "targetErrorInvalidPort": "Porta non valida", + "targetErrorInvalidPortDescription": "Inserisci un numero di porta valido", + "targetErrorNoSite": "Nessun sito selezionato", + "targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo", + "targetCreated": "Destinazione creata", + "targetCreatedDescription": "L'obiettivo è stato creato con successo", + "targetErrorCreate": "Impossibile creare l'obiettivo", + "targetErrorCreateDescription": "Si è verificato un errore durante la creazione del target", + "save": "Salva", "proxyAdditional": "Impostazioni Proxy Aggiuntive", "proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy", "proxyCustomHeader": "Intestazione Host Personalizzata", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Server Admin - Pangolina", "licenseTierProfessional": "Licenza Professional", "licenseTierEnterprise": "Licenza Enterprise", - "licenseTierCommercial": "Licenza Commerciale", + "licenseTierPersonal": "Personal License", "licensed": "Con Licenza", "yes": "Sì", "no": "No", @@ -747,7 +765,7 @@ "idpDisplayName": "Un nome visualizzato per questo provider di identità", "idpAutoProvisionUsers": "Provisioning Automatico Utenti", "idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.", - "licenseBadge": "Professionista", + "licenseBadge": "EE", "idpType": "Tipo di Provider", "idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare", "idpOidcConfigure": "Configurazione OAuth2/OIDC", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Connesso", "idpErrorConnectingTo": "Si è verificato un problema durante la connessione a {name}. Contatta il tuo amministratore.", "idpErrorNotFound": "IdP non trovato", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Invito Non Valido", "inviteInvalidDescription": "Il link di invito non è valido.", "inviteErrorWrongUser": "L'invito non è per questo utente", @@ -1083,7 +1099,6 @@ "navbar": "Menu di Navigazione", "navbarDescription": "Menu di navigazione principale dell'applicazione", "navbarDocsLink": "Documentazione", - "commercialEdition": "Edizione Commerciale", "otpErrorEnable": "Impossibile abilitare 2FA", "otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA", "otpSetupCheckCode": "Inserisci un codice a 6 cifre", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Tutti Gli Utenti", "sidebarIdentityProviders": "Fornitori Di Identità", "sidebarLicense": "Licenza", - "sidebarClients": "Clienti (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domini", "enableDockerSocket": "Abilita Progetto Docker", "enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.", @@ -1220,7 +1235,7 @@ "orgBillingDescription": "Gestisci le tue informazioni di fatturazione e abbonamenti", "github": "GitHub", "pangolinHosted": "Pangolin Hosted", - "fossorial": "Fossorial", + "fossorial": "Fossoriale", "completeAccountSetup": "Completa la Configurazione dell'Account", "completeAccountSetupDescription": "Imposta la tua password per iniziare", "accountSetupSent": "Invieremo un codice di configurazione dell'account a questo indirizzo email.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Sottodominio: {subdomain}", "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mostra Altro", + "regionSelectorTitle": "Seleziona regione", + "regionSelectorInfo": "Selezionare una regione ci aiuta a fornire migliori performance per la tua posizione. Non devi necessariamente essere nella stessa regione del tuo server.", + "regionSelectorPlaceholder": "Scegli una regione", + "regionSelectorComingSoon": "Prossimamente", + "billingLoadingSubscription": "Caricamento abbonamento...", + "billingFreeTier": "Piano Gratuito", + "billingWarningOverLimit": "Avviso: Hai superato uno o più limiti di utilizzo. I tuoi siti non si connetteranno finché non modifichi il tuo abbonamento o non adegui il tuo utilizzo.", + "billingUsageLimitsOverview": "Panoramica dei Limiti di Utilizzo", + "billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@fossorial.io.", + "billingDataUsage": "Utilizzo dei Dati", + "billingOnlineTime": "Tempo Online del Sito", + "billingUsers": "Utenti Attivi", + "billingDomains": "Domini Attivi", + "billingRemoteExitNodes": "Nodi Self-hosted Attivi", + "billingNoLimitConfigured": "Nessun limite configurato", + "billingEstimatedPeriod": "Periodo di Fatturazione Stimato", + "billingIncludedUsage": "Utilizzo Incluso", + "billingIncludedUsageDescription": "Utilizzo incluso nel tuo piano di abbonamento corrente", + "billingFreeTierIncludedUsage": "Elenchi di utilizzi inclusi nel piano gratuito", + "billingIncluded": "incluso", + "billingEstimatedTotal": "Totale Stimato:", + "billingNotes": "Note", + "billingEstimateNote": "Questa è una stima basata sul tuo utilizzo attuale.", + "billingActualChargesMayVary": "I costi effettivi possono variare.", + "billingBilledAtEnd": "Sarai fatturato alla fine del periodo di fatturazione.", + "billingModifySubscription": "Modifica Abbonamento", + "billingStartSubscription": "Inizia Abbonamento", + "billingRecurringCharge": "Addebito Ricorrente", + "billingManageSubscriptionSettings": "Gestisci impostazioni e preferenze dell'abbonamento", + "billingNoActiveSubscription": "Non hai un abbonamento attivo. Avvia il tuo abbonamento per aumentare i limiti di utilizzo.", + "billingFailedToLoadSubscription": "Caricamento abbonamento fallito", + "billingFailedToLoadUsage": "Caricamento utilizzo fallito", + "billingFailedToGetCheckoutUrl": "Errore durante l'ottenimento dell'URL di pagamento", + "billingPleaseTryAgainLater": "Per favore, riprova più tardi.", + "billingCheckoutError": "Errore di Pagamento", + "billingFailedToGetPortalUrl": "Errore durante l'ottenimento dell'URL del portale", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.", "twoFactor": "Autenticazione a Due Fattori", "adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.", - "continueToApplication": "Continua all'Applicazione", "securityKeyAdd": "Aggiungi Chiave di Sicurezza", "securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza", "securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Le modifiche DNS possono richiedere del tempo per propagarsi in Internet. Questo può richiedere da pochi minuti a 48 ore, a seconda del tuo provider DNS e delle impostazioni TTL.", "resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP", "resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP", + "billingPricingCalculatorLink": "Calcolatore di Prezzi", "signUpTerms": { "IAgreeToThe": "Accetto i", "termsOfService": "termini di servizio", @@ -1327,7 +1384,7 @@ "privacyPolicy": "informativa sulla privacy" }, "siteRequired": "Il sito è richiesto.", - "olmTunnel": "Olm Tunnel", + "olmTunnel": "Tunnel Olm", "olmTunnelDescription": "Usa Olm per la connettività client", "errorCreatingClient": "Errore nella creazione del client", "clientDefaultsNotFound": "Impostazioni predefinite del client non trovate", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Proxy Esterno Abilitato", "addNewTarget": "Aggiungi Nuovo Target", "targetsList": "Elenco dei Target", + "advancedMode": "Modalità Avanzata", "targetErrorDuplicateTargetFound": "Target duplicato trovato", + "healthCheckHealthy": "Sano", + "healthCheckUnhealthy": "Non Sano", + "healthCheckUnknown": "Sconosciuto", + "healthCheck": "Controllo Salute", + "configureHealthCheck": "Configura Controllo Salute", + "configureHealthCheckDescription": "Imposta il monitoraggio della salute per {target}", + "enableHealthChecks": "Abilita i Controlli di Salute", + "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", + "healthCheckPath": "Percorso", + "healthHostname": "IP / Nome host", + "healthPort": "Porta", + "healthCheckPathDescription": "Percorso per verificare lo stato di salute.", + "healthyIntervalSeconds": "Intervallo Sano", + "unhealthyIntervalSeconds": "Intervallo Non Sano", + "IntervalSeconds": "Intervallo Sano", + "timeoutSeconds": "Timeout", + "timeIsInSeconds": "Il tempo è in secondi", + "retryAttempts": "Tentativi di Riprova", + "expectedResponseCodes": "Codici di Risposta Attesi", + "expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.", + "customHeaders": "Intestazioni Personalizzate", + "customHeadersDescription": "Intestazioni nuova riga separate: Intestazione-Nome: valore", + "headersValidationError": "Le intestazioni devono essere nel formato: Intestazione-Nome: valore.", + "saveHealthCheck": "Salva Controllo Salute", + "healthCheckSaved": "Controllo Salute Salvato", + "healthCheckSavedDescription": "La configurazione del controllo salute è stata salvata con successo", + "healthCheckError": "Errore Controllo Salute", + "healthCheckErrorDescription": "Si è verificato un errore durante il salvataggio della configurazione del controllo salute.", + "healthCheckPathRequired": "Il percorso del controllo salute è richiesto", + "healthCheckMethodRequired": "Metodo HTTP richiesto", + "healthCheckIntervalMin": "L'intervallo del controllo deve essere almeno di 5 secondi", + "healthCheckTimeoutMin": "Il timeout deve essere di almeno 1 secondo", + "healthCheckRetryMin": "I tentativi di riprova devono essere almeno 1", "httpMethod": "Metodo HTTP", "selectHttpMethod": "Seleziona metodo HTTP", "domainPickerSubdomainLabel": "Sottodominio", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Inserisci un sottodominio per cercare e selezionare dai domini gratuiti disponibili.", "domainPickerFreeDomains": "Domini Gratuiti", "domainPickerSearchForAvailableDomains": "Cerca domini disponibili", + "domainPickerNotWorkSelfHosted": "Nota: I domini forniti gratuitamente non sono disponibili per le istanze self-hosted al momento.", "resourceDomain": "Dominio", "resourceEditDomain": "Modifica Dominio", "siteName": "Nome del Sito", @@ -1463,6 +1557,72 @@ "autoLoginError": "Errore di Accesso Automatico", "autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.", "autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.", + "remoteExitNodeManageRemoteExitNodes": "Nodi Remoti", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nodi", + "searchRemoteExitNodes": "Cerca nodi...", + "remoteExitNodeAdd": "Aggiungi Nodo", + "remoteExitNodeErrorDelete": "Errore nell'eliminare il nodo", + "remoteExitNodeQuestionRemove": "Sei sicuro di voler rimuovere il nodo {selectedNode} dall'organizzazione?", + "remoteExitNodeMessageRemove": "Una volta rimosso, il nodo non sarà più accessibile.", + "remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.", + "remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo", + "remoteExitNodeDelete": "Elimina Nodo", + "sidebarRemoteExitNodes": "Nodi Remoti", + "remoteExitNodeCreate": { + "title": "Crea Nodo", + "description": "Crea un nuovo nodo per estendere la connettività di rete", + "viewAllButton": "Visualizza Tutti I Nodi", + "strategy": { + "title": "Strategia di Creazione", + "description": "Scegli questa opzione per configurare manualmente il nodo o generare nuove credenziali.", + "adopt": { + "title": "Adotta Nodo", + "description": "Scegli questo se hai già le credenziali per il nodo." + }, + "generate": { + "title": "Genera Chiavi", + "description": "Scegli questa opzione se vuoi generare nuove chiavi per il nodo" + } + }, + "adopt": { + "title": "Adotta Nodo Esistente", + "description": "Inserisci le credenziali del nodo esistente che vuoi adottare", + "nodeIdLabel": "ID Nodo", + "nodeIdDescription": "L'ID del nodo esistente che si desidera adottare", + "secretLabel": "Segreto", + "secretDescription": "La chiave segreta del nodo esistente", + "submitButton": "Adotta Nodo" + }, + "generate": { + "title": "Credenziali Generate", + "description": "Usa queste credenziali generate per configurare il nodo", + "nodeIdTitle": "ID Nodo", + "secretTitle": "Segreto", + "saveCredentialsTitle": "Aggiungi Credenziali alla Configurazione", + "saveCredentialsDescription": "Aggiungi queste credenziali al tuo file di configurazione del nodo self-hosted Pangolin per completare la connessione.", + "submitButton": "Crea Nodo" + }, + "validation": { + "adoptRequired": "L'ID del nodo e il segreto sono necessari quando si adotta un nodo esistente" + }, + "errors": { + "loadDefaultsFailed": "Caricamento impostazioni predefinite fallito", + "defaultsNotLoaded": "Impostazioni predefinite non caricate", + "createFailed": "Impossibile creare il nodo" + }, + "success": { + "created": "Nodo creato con successo" + } + }, + "remoteExitNodeSelection": "Selezione Nodo", + "remoteExitNodeSelectionDescription": "Seleziona un nodo per instradare il traffico per questo sito locale", + "remoteExitNodeRequired": "Un nodo deve essere selezionato per i siti locali", + "noRemoteExitNodesAvailable": "Nessun Nodo Disponibile", + "noRemoteExitNodesAvailableDescription": "Non ci sono nodi disponibili per questa organizzazione. Crea un nodo prima per usare i siti locali.", + "exitNode": "Nodo di Uscita", + "country": "Paese", + "rulesMatchCountry": "Attualmente basato sull'IP di origine", "managedSelfHosted": { "title": "Gestito Auto-Ospitato", "description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Dominio Internazionale Rilevato", "willbestoredas": "Verrà conservato come:", + "roleMappingDescription": "Determinare come i ruoli sono assegnati agli utenti quando accedono quando è abilitata la fornitura automatica.", + "selectRole": "Seleziona un ruolo", + "roleMappingExpression": "Espressione", + "selectRolePlaceholder": "Scegli un ruolo", + "selectRoleDescription": "Seleziona un ruolo da assegnare a tutti gli utenti da questo provider di identità", + "roleMappingExpressionDescription": "Inserire un'espressione JMESPath per estrarre le informazioni sul ruolo dal token ID", + "idpTenantIdRequired": "L'ID dell'inquilino è obbligatorio", + "invalidValue": "Valore non valido", + "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", + "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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Google OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Intestazioni Personalizzate", - "headersValidationError": "Le intestazioni devono essere nel formato: Intestazione-Nome: valore.", + "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", + "authPageDomain": "Dominio Pagina Auth", + "noDomainSet": "Nessun dominio impostato", + "changeDomain": "Cambia Dominio", + "selectDomain": "Seleziona Dominio", + "restartCertificate": "Riavvia Certificato", + "editAuthPageDomain": "Modifica Dominio Pagina Auth", + "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", + "selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione", "domainPickerProvidedDomain": "Dominio Fornito", "domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito", "domainPickerVerified": "Verificato", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" non può essere reso valido per {domain}.", "domainPickerSubdomainSanitized": "Sottodominio igienizzato", "domainPickerSubdomainCorrected": "\"{sub}\" è stato corretto in \"{sanitized}\"", + "orgAuthSignInTitle": "Accedi alla tua 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", + "subscriptionRequiredToUse": "Per utilizzare questa funzionalità è necessario un abbonamento.", + "idpDisabled": "I provider di identità sono disabilitati.", + "orgAuthPageDisabled": "La pagina di autenticazione dell'organizzazione è disabilitata.", + "domainRestartedDescription": "Verifica del dominio riavviata con successo", "resourceAddEntrypointsEditFile": "Modifica file: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Modifica file: docker-compose.yml" + "resourceExposePortsEditFile": "Modifica file: docker-compose.yml", + "emailVerificationRequired": "Verifica via email. Effettua nuovamente il login via {dashboardUrl}/auth/login completa questo passaggio. Quindi, torna qui.", + "twoFactorSetupRequired": "È richiesta la configurazione di autenticazione a due fattori. Effettua nuovamente l'accesso tramite {dashboardUrl}/auth/login completa questo passaggio. Quindi, torna qui.", + "authPageErrorUpdateMessage": "Si è verificato un errore durante l'aggiornamento delle impostazioni della pagina di autenticazione", + "authPageUpdated": "Pagina di autenticazione aggiornata con successo", + "healthCheckNotAvailable": "Locale", + "rewritePath": "Riscrivi percorso", + "rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target.", + "continueToApplication": "Continua con l'applicazione", + "checkingInvite": "Controllo Invito", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Rimuovi Autenticazione Intestazione", + "resourceHeaderAuthRemoveDescription": "Autenticazione intestazione rimossa con successo.", + "resourceErrorHeaderAuthRemove": "Impossibile rimuovere l'autenticazione dell'intestazione", + "resourceErrorHeaderAuthRemoveDescription": "Impossibile rimuovere l'autenticazione dell'intestazione per la risorsa.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Impossibile impostare l'autenticazione dell'intestazione", + "resourceErrorHeaderAuthSetupDescription": "Impossibile impostare l'autenticazione dell'intestazione per la risorsa.", + "resourceHeaderAuthSetup": "Autenticazione intestazione impostata con successo", + "resourceHeaderAuthSetupDescription": "L'autenticazione dell'intestazione è stata impostata correttamente.", + "resourceHeaderAuthSetupTitle": "Imposta Autenticazione Intestazione", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Imposta Autenticazione Intestazione", + "actionSetResourceHeaderAuth": "Imposta Autenticazione Intestazione", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priorità", + "priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 64a449d0..a1f2d451 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -96,7 +96,7 @@ "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", "siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.", "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", - "siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "모든 사이트 보기", "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", "siteNewtCredentials": "Newt 자격 증명", @@ -168,6 +168,9 @@ "siteSelect": "사이트 선택", "siteSearch": "사이트 검색", "siteNotFound": "사이트를 찾을 수 없습니다.", + "selectCountry": "국가 선택하기", + "searchCountries": "국가 검색...", + "noCountryFound": "국가를 찾을 수 없습니다.", "siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.", "resourceType": "리소스 유형", "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", @@ -465,7 +468,10 @@ "createdAt": "생성일", "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", - "proxyEnableSSL": "SSL 활성화 (https)", + "proxyEnableSSL": "SSL 활성화", + "proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.", + "target": "대상", + "configureTarget": "대상 구성", "targetErrorFetch": "대상 가져오는 데 실패했습니다.", "targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다", "siteErrorFetch": "리소스를 가져오는 데 실패했습니다", @@ -492,7 +498,7 @@ "targetTlsSettings": "보안 연결 구성", "targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성", "targetTlsSettingsAdvanced": "고급 TLS 설정", - "targetTlsSni": "TLS 서버 이름 (SNI)", + "targetTlsSni": "TLS 서버 이름", "targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", "targetTlsSubmit": "설정 저장", "targets": "대상 구성", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", "methodSelect": "선택 방법", "targetSubmit": "대상 추가", - "targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.", + "targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.", "targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.", "targetsSubmit": "대상 저장", + "addTarget": "대상 추가", + "targetErrorInvalidIp": "유효하지 않은 IP 주소", + "targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.", + "targetErrorInvalidPort": "유효하지 않은 포트", + "targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.", + "targetErrorNoSite": "선택된 사이트 없음", + "targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.", + "targetCreated": "대상 생성", + "targetCreatedDescription": "대상이 성공적으로 생성되었습니다.", + "targetErrorCreate": "대상 생성 실패", + "targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.", + "save": "저장", "proxyAdditional": "추가 프록시 설정", "proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성", "proxyCustomHeader": "사용자 정의 호스트 헤더", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "서버 관리자 - 판골린", "licenseTierProfessional": "전문 라이센스", "licenseTierEnterprise": "기업 라이선스", - "licenseTierCommercial": "상업용 라이선스", + "licenseTierPersonal": "Personal License", "licensed": "라이센스", "yes": "예", "no": "아니요", @@ -747,7 +765,7 @@ "idpDisplayName": "이 신원 공급자를 위한 표시 이름", "idpAutoProvisionUsers": "사용자 자동 프로비저닝", "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", - "licenseBadge": "전문가", + "licenseBadge": "EE", "idpType": "제공자 유형", "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", "idpOidcConfigure": "OAuth2/OIDC 구성", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "연결됨", "idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.", "idpErrorNotFound": "IdP를 찾을 수 없습니다.", - "idpGoogleAlt": "구글", - "idpAzureAlt": "애저", "inviteInvalid": "유효하지 않은 초대", "inviteInvalidDescription": "초대 링크가 유효하지 않습니다.", "inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다", @@ -1083,7 +1099,6 @@ "navbar": "탐색 메뉴", "navbarDescription": "애플리케이션의 주요 탐색 메뉴", "navbarDocsLink": "문서", - "commercialEdition": "상업용 에디션", "otpErrorEnable": "2FA를 활성화할 수 없습니다.", "otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다", "otpSetupCheckCode": "6자리 코드를 입력하세요", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "모든 사용자", "sidebarIdentityProviders": "신원 공급자", "sidebarLicense": "라이선스", - "sidebarClients": "클라이언트 (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "도메인", "enableDockerSocket": "Docker 청사진 활성화", "enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "서브도메인: {subdomain}", "domainPickerNamespace": "이름 공간: {namespace}", "domainPickerShowMore": "더보기", + "regionSelectorTitle": "지역 선택", + "regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.", + "regionSelectorPlaceholder": "지역 선택", + "regionSelectorComingSoon": "곧 출시 예정", + "billingLoadingSubscription": "구독 불러오는 중...", + "billingFreeTier": "무료 티어", + "billingWarningOverLimit": "경고: 하나 이상의 사용 한도를 초과했습니다. 구독을 수정하거나 사용량을 조정하기 전까지 사이트는 연결되지 않습니다.", + "billingUsageLimitsOverview": "사용 한도 개요", + "billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@fossorial.io로 연락하십시오.", + "billingDataUsage": "데이터 사용량", + "billingOnlineTime": "사이트 온라인 시간", + "billingUsers": "활성 사용자", + "billingDomains": "활성 도메인", + "billingRemoteExitNodes": "활성 자체 호스팅 노드", + "billingNoLimitConfigured": "구성된 한도가 없습니다.", + "billingEstimatedPeriod": "예상 청구 기간", + "billingIncludedUsage": "포함 사용량", + "billingIncludedUsageDescription": "현재 구독 계획에 포함된 사용량", + "billingFreeTierIncludedUsage": "무료 티어 사용 허용량", + "billingIncluded": "포함됨", + "billingEstimatedTotal": "예상 총액:", + "billingNotes": "노트", + "billingEstimateNote": "현재 사용량을 기반으로 한 추정치입니다.", + "billingActualChargesMayVary": "실제 청구 금액은 다를 수 있습니다.", + "billingBilledAtEnd": "청구 기간이 끝난 후 청구됩니다.", + "billingModifySubscription": "구독 수정", + "billingStartSubscription": "구독 시작", + "billingRecurringCharge": "반복 요금", + "billingManageSubscriptionSettings": "구독 설정 및 기본 설정을 관리합니다", + "billingNoActiveSubscription": "활성 구독이 없습니다. 사용 한도를 늘리려면 구독을 시작하십시오.", + "billingFailedToLoadSubscription": "구독을 불러오는 데 실패했습니다.", + "billingFailedToLoadUsage": "사용량을 불러오는 데 실패했습니다.", + "billingFailedToGetCheckoutUrl": "체크아웃 URL을 가져오는 데 실패했습니다.", + "billingPleaseTryAgainLater": "나중에 다시 시도하십시오.", + "billingCheckoutError": "체크아웃 오류", + "billingFailedToGetPortalUrl": "포털 URL을 가져오는 데 실패했습니다.", + "billingPortalError": "포털 오류", + "billingDataUsageInfo": "클라우드에 연결할 때 보안 터널을 통해 전송된 모든 데이터에 대해 비용이 청구됩니다. 여기에는 모든 사이트의 들어오고 나가는 트래픽이 포함됩니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용하는 경우 데이터는 요금이 청구되지 않습니다.", + "billingOnlineTimeInfo": "사이트가 클라우드에 연결된 시간에 따라 요금이 청구됩니다. 예를 들어, 44,640분은 사이트가 한 달 내내 24시간 작동하는 것과 같습니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용할 때 시간은 요금이 청구되지 않습니다.", + "billingUsersInfo": "조직의 사용자마다 요금이 청구됩니다. 청구는 조직의 활성 사용자 계정 수에 따라 매일 계산됩니다.", + "billingDomainInfo": "조직의 도메인마다 요금이 청구됩니다. 청구는 조직의 활성 도메인 계정 수에 따라 매일 계산됩니다.", + "billingRemoteExitNodesInfo": "조직의 관리 노드마다 요금이 청구됩니다. 청구는 조직의 활성 관리 노드 수에 따라 매일 계산됩니다.", "domainNotFound": "도메인을 찾을 수 없습니다", "domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.", "failed": "실패", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.", "twoFactor": "이중 인증", "adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.", - "continueToApplication": "응용 프로그램으로 계속", "securityKeyAdd": "보안 키 추가", "securityKeyRegisterTitle": "새 보안 키 등록", "securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.", "resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다", "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요", + "billingPricingCalculatorLink": "가격 계산기", "signUpTerms": { "IAgreeToThe": "동의합니다", "termsOfService": "서비스 약관", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "외부 프록시 활성화됨", "addNewTarget": "새 대상 추가", "targetsList": "대상 목록", + "advancedMode": "고급 모드", "targetErrorDuplicateTargetFound": "중복 대상 발견", + "healthCheckHealthy": "정상", + "healthCheckUnhealthy": "비정상", + "healthCheckUnknown": "알 수 없음", + "healthCheck": "상태 확인", + "configureHealthCheck": "상태 확인 설정", + "configureHealthCheckDescription": "{target}에 대한 상태 모니터링 설정", + "enableHealthChecks": "상태 확인 활성화", + "enableHealthChecksDescription": "이 대상을 모니터링하여 건강 상태를 확인하세요. 필요에 따라 대상과 다른 엔드포인트를 모니터링할 수 있습니다.", + "healthScheme": "방법", + "healthSelectScheme": "방법 선택", + "healthCheckPath": "경로", + "healthHostname": "IP / 호스트", + "healthPort": "포트", + "healthCheckPathDescription": "상태 확인을 위한 경로입니다.", + "healthyIntervalSeconds": "정상 간격", + "unhealthyIntervalSeconds": "비정상 간격", + "IntervalSeconds": "정상 간격", + "timeoutSeconds": "시간 초과", + "timeIsInSeconds": "시간은 초 단위입니다", + "retryAttempts": "재시도 횟수", + "expectedResponseCodes": "예상 응답 코드", + "expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.", + "customHeaders": "사용자 정의 헤더", + "customHeadersDescription": "헤더는 새 줄로 구분됨: Header-Name: value", + "headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.", + "saveHealthCheck": "상태 확인 저장", + "healthCheckSaved": "상태 확인이 저장되었습니다.", + "healthCheckSavedDescription": "상태 확인 구성이 성공적으로 저장되었습니다", + "healthCheckError": "상태 확인 오류", + "healthCheckErrorDescription": "상태 확인 구성을 저장하는 동안 오류가 발생했습니다", + "healthCheckPathRequired": "상태 확인 경로는 필수입니다.", + "healthCheckMethodRequired": "HTTP 방법은 필수입니다.", + "healthCheckIntervalMin": "확인 간격은 최소 5초여야 합니다.", + "healthCheckTimeoutMin": "시간 초과는 최소 1초여야 합니다.", + "healthCheckRetryMin": "재시도 횟수는 최소 1회여야 합니다.", "httpMethod": "HTTP 메소드", "selectHttpMethod": "HTTP 메소드 선택", "domainPickerSubdomainLabel": "서브도메인", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "사용 가능한 무료 도메인에서 검색 및 선택할 서브도메인 입력.", "domainPickerFreeDomains": "무료 도메인", "domainPickerSearchForAvailableDomains": "사용 가능한 도메인 검색", + "domainPickerNotWorkSelfHosted": "참고: 무료 제공 도메인은 현재 자체 호스팅 인스턴스에 사용할 수 없습니다.", "resourceDomain": "도메인", "resourceEditDomain": "도메인 수정", "siteName": "사이트 이름", @@ -1463,6 +1557,72 @@ "autoLoginError": "자동 로그인 오류", "autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.", "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.", + "remoteExitNodeManageRemoteExitNodes": "원격 노드", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "노드", + "searchRemoteExitNodes": "노드 검색...", + "remoteExitNodeAdd": "노드 추가", + "remoteExitNodeErrorDelete": "노드 삭제 오류", + "remoteExitNodeQuestionRemove": "조직에서 노드 {selectedNode}를 제거하시겠습니까?", + "remoteExitNodeMessageRemove": "한 번 제거되면 더 이상 노드에 접근할 수 없습니다.", + "remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.", + "remoteExitNodeConfirmDelete": "노드 삭제 확인", + "remoteExitNodeDelete": "노드 삭제", + "sidebarRemoteExitNodes": "원격 노드", + "remoteExitNodeCreate": { + "title": "노드 생성", + "description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요", + "viewAllButton": "모든 노드 보기", + "strategy": { + "title": "생성 전략", + "description": "노드를 직접 구성하거나 새 자격 증명을 생성하려면 이것을 선택하세요.", + "adopt": { + "title": "노드 채택", + "description": "이미 노드의 자격 증명이 있는 경우 이것을 선택하세요." + }, + "generate": { + "title": "키 생성", + "description": "노드에 대한 새 키를 생성하려면 이것을 선택하세요" + } + }, + "adopt": { + "title": "기존 노드 채택", + "description": "채택하려는 기존 노드의 자격 증명을 입력하세요", + "nodeIdLabel": "노드 ID", + "nodeIdDescription": "채택하려는 기존 노드의 ID", + "secretLabel": "비밀", + "secretDescription": "기존 노드의 비밀 키", + "submitButton": "노드 채택" + }, + "generate": { + "title": "생성된 자격 증명", + "description": "생성된 자격 증명을 사용하여 노드를 구성하세요", + "nodeIdTitle": "노드 ID", + "secretTitle": "비밀", + "saveCredentialsTitle": "구성에 자격 증명 추가", + "saveCredentialsDescription": "연결을 완료하려면 이러한 자격 증명을 자체 호스팅 Pangolin 노드 구성 파일에 추가하십시오.", + "submitButton": "노드 생성" + }, + "validation": { + "adoptRequired": "기존 노드를 채택하려면 노드 ID와 비밀 키가 필요합니다" + }, + "errors": { + "loadDefaultsFailed": "기본값 로드 실패", + "defaultsNotLoaded": "기본값 로드되지 않음", + "createFailed": "노드 생성 실패" + }, + "success": { + "created": "노드가 성공적으로 생성되었습니다" + } + }, + "remoteExitNodeSelection": "노드 선택", + "remoteExitNodeSelectionDescription": "이 로컬 사이트에서 트래픽을 라우팅할 노드를 선택하세요", + "remoteExitNodeRequired": "로컬 사이트에 노드를 선택해야 합니다", + "noRemoteExitNodesAvailable": "사용 가능한 노드가 없습니다", + "noRemoteExitNodesAvailableDescription": "이 조직에 사용 가능한 노드가 없습니다. 로컬 사이트를 사용하려면 먼저 노드를 생성하세요.", + "exitNode": "종단 노드", + "country": "국가", + "rulesMatchCountry": "현재 소스 IP를 기반으로 합니다", "managedSelfHosted": { "title": "관리 자체 호스팅", "description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "국제 도메인 감지됨", "willbestoredas": "다음으로 저장됩니다:", + "roleMappingDescription": "자동 프로비저닝이 활성화되면 사용자가 로그인할 때 역할이 할당되는 방법을 결정합니다.", + "selectRole": "역할 선택", + "roleMappingExpression": "표현식", + "selectRolePlaceholder": "역할 선택", + "selectRoleDescription": "이 신원 공급자로부터 모든 사용자에게 할당할 역할을 선택하십시오.", + "roleMappingExpressionDescription": "ID 토큰에서 역할 정보를 추출하기 위한 JMESPath 표현식을 입력하세요.", + "idpTenantIdRequired": "테넌트 ID가 필요합니다", + "invalidValue": "잘못된 값", + "idpTypeLabel": "신원 공급자 유형", + "roleMappingExpressionPlaceholder": "예: contains(groups, 'admin') && 'Admin' || 'Member'", + "idpGoogleConfiguration": "Google 구성", + "idpGoogleConfigurationDescription": "Google OAuth2 자격 증명을 구성합니다.", + "idpGoogleClientIdDescription": "Google OAuth2 클라이언트 ID", + "idpGoogleClientSecretDescription": "Google OAuth2 클라이언트 비밀", + "idpAzureConfiguration": "Azure Entra ID 구성", + "idpAzureConfigurationDescription": "Azure Entra ID OAuth2 자격 증명을 구성합니다.", + "idpTenantId": "테넌트 ID", + "idpTenantIdPlaceholder": "your-tenant-id", + "idpAzureTenantIdDescription": "Azure 액티브 디렉터리 개요에서 찾은 Azure 테넌트 ID", + "idpAzureClientIdDescription": "Azure 앱 등록 클라이언트 ID", + "idpAzureClientSecretDescription": "Azure 앱 등록 클라이언트 비밀", + "idpGoogleTitle": "구글", + "idpGoogleAlt": "구글", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "애저", + "idpGoogleConfigurationTitle": "Google 구성", + "idpAzureConfigurationTitle": "Azure Entra ID 구성", + "idpTenantIdLabel": "테넌트 ID", + "idpAzureClientIdDescription2": "Azure 앱 등록 클라이언트 ID", + "idpAzureClientSecretDescription2": "Azure 앱 등록 클라이언트 비밀", "idpGoogleDescription": "Google OAuth2/OIDC 공급자", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자", - "customHeaders": "사용자 정의 헤더", - "headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.", + "subnet": "서브넷", + "subnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.", + "authPage": "인증 페이지", + "authPageDescription": "조직에 대한 인증 페이지를 구성합니다.", + "authPageDomain": "인증 페이지 도메인", + "noDomainSet": "도메인 설정 없음", + "changeDomain": "도메인 변경", + "selectDomain": "도메인 선택", + "restartCertificate": "인증서 재시작", + "editAuthPageDomain": "인증 페이지 도메인 편집", + "setAuthPageDomain": "인증 페이지 도메인 설정", + "failedToFetchCertificate": "인증서 가져오기 실패", + "failedToRestartCertificate": "인증서 재시작 실패", + "addDomainToEnableCustomAuthPages": "조직의 맞춤 인증 페이지를 활성화하려면 도메인을 추가하세요.", + "selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.", "domainPickerProvidedDomain": "제공된 도메인", "domainPickerFreeProvidedDomain": "무료 제공된 도메인", "domainPickerVerified": "검증됨", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\"을(를) {domain}에 대해 유효하게 만들 수 없습니다.", "domainPickerSubdomainSanitized": "하위 도메인 정리됨", "domainPickerSubdomainCorrected": "\"{sub}\"이(가) \"{sanitized}\"로 수정되었습니다", + "orgAuthSignInTitle": "조직에 로그인", + "orgAuthChooseIdpDescription": "계속하려면 신원 공급자를 선택하세요.", + "orgAuthNoIdpConfigured": "이 조직은 구성된 신원 공급자가 없습니다. 대신 Pangolin 아이덴티티로 로그인할 수 있습니다.", + "orgAuthSignInWithPangolin": "Pangolin으로 로그인", + "subscriptionRequiredToUse": "이 기능을 사용하려면 구독이 필요합니다.", + "idpDisabled": "신원 공급자가 비활성화되었습니다.", + "orgAuthPageDisabled": "조직 인증 페이지가 비활성화되었습니다.", + "domainRestartedDescription": "도메인 인증이 성공적으로 재시작되었습니다.", "resourceAddEntrypointsEditFile": "파일 편집: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "파일 편집: docker-compose.yml" + "resourceExposePortsEditFile": "파일 편집: docker-compose.yml", + "emailVerificationRequired": "이메일 인증이 필요합니다. 이 단계를 완료하려면 {dashboardUrl}/auth/login 통해 다시 로그인하십시오. 그런 다음 여기로 돌아오세요.", + "twoFactorSetupRequired": "이중 인증 설정이 필요합니다. 이 단계를 완료하려면 {dashboardUrl}/auth/login 통해 다시 로그인하십시오. 그런 다음 여기로 돌아오세요.", + "authPageErrorUpdateMessage": "인증 페이지 설정을 업데이트하는 동안 오류가 발생했습니다", + "authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다", + "healthCheckNotAvailable": "로컬", + "rewritePath": "경로 재작성", + "rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다.", + "continueToApplication": "응용 프로그램으로 계속", + "checkingInvite": "초대 확인 중", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "헤더 인증 제거", + "resourceHeaderAuthRemoveDescription": "헤더 인증이 성공적으로 제거되었습니다.", + "resourceErrorHeaderAuthRemove": "헤더 인증 제거 실패", + "resourceErrorHeaderAuthRemoveDescription": "리소스의 헤더 인증을 제거할 수 없습니다.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "헤더 인증 설정 실패", + "resourceErrorHeaderAuthSetupDescription": "리소스의 헤더 인증을 설정할 수 없습니다.", + "resourceHeaderAuthSetup": "헤더 인증이 성공적으로 설정되었습니다.", + "resourceHeaderAuthSetupDescription": "헤더 인증이 성공적으로 설정되었습니다.", + "resourceHeaderAuthSetupTitle": "헤더 인증 설정", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "헤더 인증 설정", + "actionSetResourceHeaderAuth": "헤더 인증 설정", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "우선순위", + "priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index ef5c0d2a..ad8eb643 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -96,7 +96,7 @@ "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": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Se alle områder", "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", "siteNewtCredentials": "Newt påloggingsinformasjon", @@ -118,7 +118,7 @@ "usageExamples": "Brukseksempler", "tokenId": "Token-ID", "requestHeades": "Request Headers", - "queryParameter": "Query Parameter", + "queryParameter": "Forespørsel Params", "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", @@ -168,6 +168,9 @@ "siteSelect": "Velg område", "siteSearch": "Søk i område", "siteNotFound": "Ingen område funnet.", + "selectCountry": "Velg land", + "searchCountries": "Søk land...", + "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", @@ -465,7 +468,10 @@ "createdAt": "Opprettet", "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", - "proxyEnableSSL": "Aktiver SSL (https)", + "proxyEnableSSL": "Aktiver SSL", + "proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.", + "target": "Target", + "configureTarget": "Konfigurer mål", "targetErrorFetch": "Kunne ikke hente mål", "targetErrorFetchDescription": "Det oppsto en feil under henting av mål", "siteErrorFetch": "Klarte ikke å hente ressurs", @@ -492,7 +498,7 @@ "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", - "targetTlsSni": "TLS Servernavn (SNI)", + "targetTlsSni": "TLS servernavn", "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", "targetTlsSubmit": "Lagre innstillinger", "targets": "Målkonfigurasjon", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", "methodSelect": "Velg metode", "targetSubmit": "Legg til mål", - "targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.", + "targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.", "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", "targetsSubmit": "Lagre mål", + "addTarget": "Legg til mål", + "targetErrorInvalidIp": "Ugyldig IP-adresse", + "targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn", + "targetErrorInvalidPort": "Ugyldig port", + "targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer", + "targetErrorNoSite": "Ingen nettsted valgt", + "targetErrorNoSiteDescription": "Velg et nettsted for målet", + "targetCreated": "Mål opprettet", + "targetCreatedDescription": "Målet har blitt opprettet", + "targetErrorCreate": "Kunne ikke opprette målet", + "targetErrorCreateDescription": "Det oppstod en feil under oppretting av målet", + "save": "Lagre", "proxyAdditional": "Ytterligere Proxy-innstillinger", "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", "proxyCustomHeader": "Tilpasset verts-header", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Server Admin - Pangolin", "licenseTierProfessional": "Profesjonell lisens", "licenseTierEnterprise": "Bedriftslisens", - "licenseTierCommercial": "Kommersiell lisens", + "licenseTierPersonal": "Personal License", "licensed": "Lisensiert", "yes": "Ja", "no": "Nei", @@ -747,7 +765,7 @@ "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", "idpAutoProvisionUsers": "Automatisk brukerklargjøring", "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", - "licenseBadge": "Profesjonell", + "licenseBadge": "EE", "idpType": "Leverandørtype", "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Tilkoblet", "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", "idpErrorNotFound": "IdP ikke funnet", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Ugyldig invitasjon", "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", @@ -1083,7 +1099,6 @@ "navbar": "Navigasjonsmeny", "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", "navbarDocsLink": "Dokumentasjon", - "commercialEdition": "Kommersiell utgave", "otpErrorEnable": "Kunne ikke aktivere 2FA", "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Alle brukere", "sidebarIdentityProviders": "Identitetsleverandører", "sidebarLicense": "Lisens", - "sidebarClients": "Klienter (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domener", "enableDockerSocket": "Aktiver Docker blåkopi", "enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Underdomene: {subdomain}", "domainPickerNamespace": "Navnerom: {namespace}", "domainPickerShowMore": "Vis mer", + "regionSelectorTitle": "Velg Region", + "regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.", + "regionSelectorPlaceholder": "Velg en region", + "regionSelectorComingSoon": "Kommer snart", + "billingLoadingSubscription": "Laster abonnement...", + "billingFreeTier": "Gratis nivå", + "billingWarningOverLimit": "Advarsel: Du har overskredet en eller flere bruksgrenser. Nettstedene dine vil ikke koble til før du endrer abonnementet ditt eller justerer bruken.", + "billingUsageLimitsOverview": "Oversikt over bruksgrenser", + "billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@fossorial.io.", + "billingDataUsage": "Databruk", + "billingOnlineTime": "Online tid for nettsteder", + "billingUsers": "Aktive brukere", + "billingDomains": "Aktive domener", + "billingRemoteExitNodes": "Aktive selvstyrte noder", + "billingNoLimitConfigured": "Ingen grense konfigurert", + "billingEstimatedPeriod": "Estimert faktureringsperiode", + "billingIncludedUsage": "Inkludert Bruk", + "billingIncludedUsageDescription": "Bruk inkludert i din nåværende abonnementsplan", + "billingFreeTierIncludedUsage": "Gratis nivå bruksgrenser", + "billingIncluded": "inkludert", + "billingEstimatedTotal": "Estimert Totalt:", + "billingNotes": "Notater", + "billingEstimateNote": "Dette er et estimat basert på din nåværende bruk.", + "billingActualChargesMayVary": "Faktiske kostnader kan variere.", + "billingBilledAtEnd": "Du vil bli fakturert ved slutten av faktureringsperioden.", + "billingModifySubscription": "Endre abonnement", + "billingStartSubscription": "Start abonnement", + "billingRecurringCharge": "Innkommende Avgift", + "billingManageSubscriptionSettings": "Administrer abonnementsinnstillinger og preferanser", + "billingNoActiveSubscription": "Du har ikke et aktivt abonnement. Start abonnementet ditt for å øke bruksgrensene.", + "billingFailedToLoadSubscription": "Klarte ikke å laste abonnement", + "billingFailedToLoadUsage": "Klarte ikke å laste bruksdata", + "billingFailedToGetCheckoutUrl": "Mislyktes å få betalingslenke", + "billingPleaseTryAgainLater": "Vennligst prøv igjen senere.", + "billingCheckoutError": "Kasserror", + "billingFailedToGetPortalUrl": "Mislyktes å hente portal URL", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", "twoFactor": "Tofaktorautentisering", "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", - "continueToApplication": "Fortsett til applikasjonen", "securityKeyAdd": "Legg til sikkerhetsnøkkel", "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS-endringer kan ta litt tid å propagere over internett. Dette kan ta fra noen få minutter til 48 timer, avhengig av din DNS-leverandør og TTL-innstillinger.", "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", + "billingPricingCalculatorLink": "Pris Kalkulator", "signUpTerms": { "IAgreeToThe": "Jeg godtar", "termsOfService": "brukervilkårene", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Ekstern proxy aktivert", "addNewTarget": "Legg til nytt mål", "targetsList": "Liste over mål", + "advancedMode": "Avansert modus", "targetErrorDuplicateTargetFound": "Duplikat av mål funnet", + "healthCheckHealthy": "Sunn", + "healthCheckUnhealthy": "Usunn", + "healthCheckUnknown": "Ukjent", + "healthCheck": "Helsekontroll", + "configureHealthCheck": "Konfigurer Helsekontroll", + "configureHealthCheckDescription": "Sett opp helsekontroll for {target}", + "enableHealthChecks": "Aktiver Helsekontroller", + "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", + "healthCheckPath": "Sti", + "healthHostname": "IP / Vert", + "healthPort": "Port", + "healthCheckPathDescription": "Stien for å sjekke helsestatus.", + "healthyIntervalSeconds": "Sunt intervall", + "unhealthyIntervalSeconds": "Usunt intervall", + "IntervalSeconds": "Sunt intervall", + "timeoutSeconds": "Tidsavbrudd", + "timeIsInSeconds": "Tid er i sekunder", + "retryAttempts": "Forsøk på nytt", + "expectedResponseCodes": "Forventede svarkoder", + "expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.", + "customHeaders": "Egendefinerte topptekster", + "customHeadersDescription": "Overskrifter som er adskilt med linje: Overskriftsnavn: verdi", + "headersValidationError": "Topptekst må være i formatet: header-navn: verdi.", + "saveHealthCheck": "Lagre Helsekontroll", + "healthCheckSaved": "Helsekontroll Lagret", + "healthCheckSavedDescription": "Helsekontrollkonfigurasjonen ble lagret", + "healthCheckError": "Helsekontrollfeil", + "healthCheckErrorDescription": "Det oppstod en feil under lagring av helsekontrollkonfigurasjonen", + "healthCheckPathRequired": "Helsekontrollsti er påkrevd", + "healthCheckMethodRequired": "HTTP-metode er påkrevd", + "healthCheckIntervalMin": "Sjekkeintervallet må være minst 5 sekunder", + "healthCheckTimeoutMin": "Timeout må være minst 1 sekund", + "healthCheckRetryMin": "Forsøk på nytt må være minst 1", "httpMethod": "HTTP-metode", "selectHttpMethod": "Velg HTTP-metode", "domainPickerSubdomainLabel": "Underdomene", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Skriv inn et underdomene for å søke og velge blant tilgjengelige gratis domener.", "domainPickerFreeDomains": "Gratis domener", "domainPickerSearchForAvailableDomains": "Søk etter tilgjengelige domener", + "domainPickerNotWorkSelfHosted": "Merk: Gratis tilbudte domener er ikke tilgjengelig for selv-hostede instanser akkurat nå.", "resourceDomain": "Domene", "resourceEditDomain": "Rediger domene", "siteName": "Områdenavn", @@ -1463,6 +1557,72 @@ "autoLoginError": "Feil ved automatisk innlogging", "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.", "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.", + "remoteExitNodeManageRemoteExitNodes": "Eksterne Noder", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Noder", + "searchRemoteExitNodes": "Søk noder...", + "remoteExitNodeAdd": "Legg til Node", + "remoteExitNodeErrorDelete": "Feil ved sletting av node", + "remoteExitNodeQuestionRemove": "Er du sikker på at du vil fjerne noden {selectedNode} fra organisasjonen?", + "remoteExitNodeMessageRemove": "Når noden er fjernet, vil ikke lenger være tilgjengelig.", + "remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.", + "remoteExitNodeConfirmDelete": "Bekreft sletting av Node", + "remoteExitNodeDelete": "Slett Node", + "sidebarRemoteExitNodes": "Eksterne Noder", + "remoteExitNodeCreate": { + "title": "Opprett node", + "description": "Opprett en ny node for å utvide nettverkstilkoblingen din", + "viewAllButton": "Vis alle koder", + "strategy": { + "title": "Opprettelsesstrategi", + "description": "Velg denne for manuelt å konfigurere noden eller generere nye legitimasjoner.", + "adopt": { + "title": "Adopter Node", + "description": "Velg dette hvis du allerede har legitimasjon til noden." + }, + "generate": { + "title": "Generer Nøkler", + "description": "Velg denne hvis du vil generere nye nøkler for noden" + } + }, + "adopt": { + "title": "Adopter Eksisterende Node", + "description": "Skriv inn opplysningene til den eksisterende noden du vil adoptere", + "nodeIdLabel": "Node-ID", + "nodeIdDescription": "ID-en til den eksisterende noden du vil adoptere", + "secretLabel": "Sikkerhetsnøkkel", + "secretDescription": "Den hemmelige nøkkelen til en eksisterende node", + "submitButton": "Adopter Node" + }, + "generate": { + "title": "Genererte Legitimasjoner", + "description": "Bruk disse genererte opplysningene for å konfigurere noden din", + "nodeIdTitle": "Node-ID", + "secretTitle": "Sikkerhet", + "saveCredentialsTitle": "Legg til Legitimasjoner til Config", + "saveCredentialsDescription": "Legg til disse legitimasjonene i din selv-hostede Pangolin node-konfigurasjonsfil for å fullføre koblingen.", + "submitButton": "Opprett node" + }, + "validation": { + "adoptRequired": "Node ID og Secret er påkrevd når du adopterer en eksisterende node" + }, + "errors": { + "loadDefaultsFailed": "Feil ved lasting av standarder", + "defaultsNotLoaded": "Standarder ikke lastet", + "createFailed": "Kan ikke opprette node" + }, + "success": { + "created": "Node opprettet" + } + }, + "remoteExitNodeSelection": "Noden utvalg", + "remoteExitNodeSelectionDescription": "Velg en node for å sende trafikk gjennom for dette lokale nettstedet", + "remoteExitNodeRequired": "En node må velges for lokale nettsteder", + "noRemoteExitNodesAvailable": "Ingen noder tilgjengelig", + "noRemoteExitNodesAvailableDescription": "Ingen noder er tilgjengelige for denne organisasjonen. Opprett en node først for å bruke lokale nettsteder.", + "exitNode": "Utgangsnode", + "country": "Land", + "rulesMatchCountry": "For tiden basert på kilde IP", "managedSelfHosted": { "title": "Administrert selv-hostet", "description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Internasjonalt domene oppdaget", "willbestoredas": "Vil bli lagret som:", + "roleMappingDescription": "Bestem hvordan roller tilordnes brukere når innloggingen er aktivert når autog-rapportering er aktivert.", + "selectRole": "Velg en rolle", + "roleMappingExpression": "Uttrykk", + "selectRolePlaceholder": "Velg en rolle", + "selectRoleDescription": "Velg en rolle å tilordne alle brukere fra denne identitet leverandøren", + "roleMappingExpressionDescription": "Skriv inn et JMESPath uttrykk for å hente rolleinformasjon fra ID-nøkkelen", + "idpTenantIdRequired": "Bedriftens ID kreves", + "invalidValue": "Ugyldig verdi", + "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", + "idpAzureConfiguration": "Azure Entra ID konfigurasjon", + "idpAzureConfigurationDescription": "Konfigurere din 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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Google OAuth2/OIDC leverandør", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Egendefinerte topptekster", - "headersValidationError": "Topptekst må være i formatet: header-navn: verdi.", + "subnet": "Subnett", + "subnetDescription": "Undernettverket for denne organisasjonens nettverkskonfigurasjon.", + "authPage": "Autentiseringsside", + "authPageDescription": "Konfigurer autoriseringssiden for din organisasjon", + "authPageDomain": "Autentiseringsside domene", + "noDomainSet": "Ingen domene valgt", + "changeDomain": "Endre domene", + "selectDomain": "Velg domene", + "restartCertificate": "Omstart sertifikat", + "editAuthPageDomain": "Rediger auth sidedomene", + "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", + "selectDomainForOrgAuthPage": "Velg et domene for organisasjonens autentiseringsside", "domainPickerProvidedDomain": "Gitt domene", "domainPickerFreeProvidedDomain": "Gratis oppgitt domene", "domainPickerVerified": "Bekreftet", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Et abonnement er påkrevd for å bruke denne funksjonen.", + "idpDisabled": "Identitetsleverandører er deaktivert.", + "orgAuthPageDisabled": "Informasjons-siden for organisasjon er deaktivert.", + "domainRestartedDescription": "Domene-verifiseringen ble startet på nytt", "resourceAddEntrypointsEditFile": "Rediger fil: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Rediger fil: docker-compose.yml" + "resourceExposePortsEditFile": "Rediger fil: docker-compose.yml", + "emailVerificationRequired": "E-postbekreftelse er nødvendig. Logg inn på nytt via {dashboardUrl}/auth/login og fullfør dette trinnet. Kom deretter tilbake her.", + "twoFactorSetupRequired": "To-faktor autentiseringsoppsett er nødvendig. Vennligst logg inn igjen via {dashboardUrl}/auth/login og fullfør dette steget. Kom deretter tilbake her.", + "authPageErrorUpdateMessage": "Det oppstod en feil under oppdatering av innstillingene for godkjenningssiden", + "authPageUpdated": "Godkjenningsside oppdatert", + "healthCheckNotAvailable": "Lokal", + "rewritePath": "Omskriv sti", + "rewritePathDescription": "Valgfritt omskrive stien før videresending til målet.", + "continueToApplication": "Fortsett til applikasjonen", + "checkingInvite": "Sjekker invitasjon", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Fjern topptekst Auth", + "resourceHeaderAuthRemoveDescription": "Topplinje autentisering fjernet.", + "resourceErrorHeaderAuthRemove": "Kunne ikke fjerne topptekst autentisering", + "resourceErrorHeaderAuthRemoveDescription": "Kunne ikke fjerne topptekst autentisering for ressursen.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Kunne ikke sette topptekst autentisering", + "resourceErrorHeaderAuthSetupDescription": "Kunne ikke sette topplinje autentisering for ressursen.", + "resourceHeaderAuthSetup": "Header godkjenningssett var vellykket", + "resourceHeaderAuthSetupDescription": "Topplinje autentisering har blitt lagret.", + "resourceHeaderAuthSetupTitle": "Angi topptekst godkjenning", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Angi topptekst godkjenning", + "actionSetResourceHeaderAuth": "Angi topptekst godkjenning", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Prioritet", + "priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index ba4ab637..25181569 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -3,14 +3,14 @@ "setupNewOrg": "Nieuwe organisatie", "setupCreateOrg": "Nieuwe organisatie aanmaken", "setupCreateResources": "Bronnen aanmaken", - "setupOrgName": "Naam organisatie", + "setupOrgName": "Naam van de organisatie", "orgDisplayName": "Dit is de weergavenaam van uw organisatie.", "orgId": "Organisatie ID", "setupIdentifierMessage": "Dit is de unieke identificatie voor uw organisatie. Deze is gescheiden van de weergavenaam.", "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.", - "welcome": "Welkom bij Pangolin", + "welcome": "Welkom bij Pangolin!", "welcomeTo": "Welkom bij", "componentsCreateOrg": "Maak een Organisatie", "componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} one {één organisatie} other {# organisaties}}.", @@ -22,7 +22,7 @@ "inviteErrorUser": "Het spijt ons, maar de uitnodiging die u probeert te gebruiken is niet voor deze gebruiker.", "inviteLoginUser": "Controleer of je bent aangemeld als de juiste gebruiker.", "inviteErrorNoUser": "Het spijt ons, maar de uitnodiging die u probeert te gebruiken is niet voor een bestaande gebruiker.", - "inviteCreateUser": "U moet eerst een account aanmaken", + "inviteCreateUser": "U moet eerst een account aanmaken.", "goHome": "Ga naar huis", "inviteLogInOtherUser": "Log in als een andere gebruiker", "createAnAccount": "Account aanmaken", @@ -35,7 +35,7 @@ "createAccount": "Account Aanmaken", "viewSettings": "Instellingen weergeven", "delete": "Verwijderen", - "name": "naam", + "name": "Naam", "online": "Online", "offline": "Offline", "site": "Referentie", @@ -96,8 +96,8 @@ "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": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES", - "siteSeeAll": "Alle werkruimtes bekijken", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", + "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", @@ -146,12 +146,12 @@ "never": "Nooit", "shareErrorSelectResource": "Selecteer een bron", "resourceTitle": "Bronnen beheren", - "resourceDescription": "Veilige proxy's voor uw privé applicaties aanmaken", + "resourceDescription": "Veilige proxy's voor uw privéapplicaties maken", "resourcesSearch": "Zoek bronnen...", "resourceAdd": "Bron toevoegen", "resourceErrorDelte": "Fout bij verwijderen document", "authentication": "Authenticatie", - "protected": "Beveiligd", + "protected": "Beschermd", "notProtected": "Niet beveiligd", "resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.", "resourceMessageConfirm": "Om te bevestigen, typ de naam van de bron hieronder.", @@ -168,6 +168,9 @@ "siteSelect": "Selecteer site", "siteSearch": "Zoek site", "siteNotFound": "Geen site gevonden.", + "selectCountry": "Selecteer land", + "searchCountries": "Zoek landen...", + "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", @@ -265,7 +268,7 @@ "apiKeysGeneralSettingsDescription": "Bepaal wat deze API-sleutel kan doen", "apiKeysList": "Uw API-sleutel", "apiKeysSave": "Uw API-sleutel opslaan", - "apiKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", + "apiKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plek.", "apiKeysInfo": "Uw API-sleutel is:", "apiKeysConfirmCopy": "Ik heb de API-sleutel gekopieerd", "generate": "Genereren", @@ -465,7 +468,10 @@ "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 (https) inschakelen", + "proxyEnableSSL": "SSL inschakelen", + "proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.", + "target": "Target", + "configureTarget": "Doelstellingen configureren", "targetErrorFetch": "Ophalen van doelen mislukt", "targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten", "siteErrorFetch": "Mislukt om resource op te halen", @@ -492,7 +498,7 @@ "targetTlsSettings": "HTTPS & TLS instellingen", "targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren", "targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen", - "targetTlsSni": "TLS Server Naam (SNI)", + "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", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "methodSelect": "Selecteer methode", "targetSubmit": "Doelwit toevoegen", - "targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.", - "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal load balancering mogelijk maken.", + "targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.", + "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.", "targetsSubmit": "Doelstellingen opslaan", + "addTarget": "Doelwit toevoegen", + "targetErrorInvalidIp": "Ongeldig IP-adres", + "targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in", + "targetErrorInvalidPort": "Ongeldige poort", + "targetErrorInvalidPortDescription": "Voer een geldig poortnummer in", + "targetErrorNoSite": "Geen site geselecteerd", + "targetErrorNoSiteDescription": "Selecteer een site voor het doel", + "targetCreated": "Doel aangemaakt", + "targetCreatedDescription": "Doel is succesvol aangemaakt", + "targetErrorCreate": "Kan doel niet aanmaken", + "targetErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van het doel", + "save": "Opslaan", "proxyAdditional": "Extra Proxy-instellingen", "proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld", "proxyCustomHeader": "Aangepaste Host-header", @@ -572,7 +590,7 @@ "domainsErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de domeinen", "none": "geen", "unknown": "onbekend", - "resources": "Hulpmiddelen", + "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.", "resourcesWireGuardConnect": "Beveiligde verbinding met WireGuard versleuteling", "resourcesMultipleAuthenticationMethods": "Meerdere verificatiemethoden configureren", @@ -598,7 +616,7 @@ "newtId": "Newt-ID", "newtSecretKey": "Nieuwe geheime sleutel", "architecture": "Architectuur", - "sites": "Verbindingen", + "sites": "Sites", "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.", "siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients", "siteWgManualConfigurationRequired": "Handmatige configuratie vereist", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Serverbeheer - Pangolin", "licenseTierProfessional": "Professionele licentie", "licenseTierEnterprise": "Enterprise Licentie", - "licenseTierCommercial": "Commerciële licentie", + "licenseTierPersonal": "Personal License", "licensed": "Gelicentieerd", "yes": "ja", "no": "Neen", @@ -727,35 +745,35 @@ "idpQuestionRemove": "Weet u zeker dat u de identiteitsprovider {name} permanent wilt verwijderen?", "idpMessageRemove": "Dit zal de identiteitsprovider en alle bijbehorende configuraties verwijderen. Gebruikers die via deze provider authenticeren, kunnen niet langer inloggen.", "idpMessageConfirm": "Om dit te bevestigen, typt u de naam van onderstaande identiteitsprovider.", - "idpConfirmDelete": "Bevestig verwijderen Identity Provider", - "idpDelete": "Identity Provider verwijderen", - "idp": "Identiteitsaanbieders", - "idpSearch": "Identiteitsaanbieders zoeken...", - "idpAdd": "Identity Provider toevoegen", - "idpClientIdRequired": "Client-ID is vereist.", - "idpClientSecretRequired": "Clientgeheim is vereist.", - "idpErrorAuthUrlInvalid": "Authenticatie-URL moet een geldige URL zijn.", - "idpErrorTokenUrlInvalid": "Token-URL moet een geldige URL zijn.", + "idpConfirmDelete": "Bevestig verwijderen identiteit provider", + "idpDelete": "Identiteit provider verwijderen", + "idp": "Identiteitsproviders", + "idpSearch": "Identiteitsproviders zoeken...", + "idpAdd": "Identiteit provider toevoegen", + "idpClientIdRequired": "Client ID is vereist.", + "idpClientSecretRequired": "Client geheim is vereist.", + "idpErrorAuthUrlInvalid": "Authenticatie URL moet een geldige URL zijn.", + "idpErrorTokenUrlInvalid": "Token URL moet een geldige URL zijn.", "idpPathRequired": "ID-pad is vereist.", "idpScopeRequired": "Toepassingsgebieden zijn vereist.", - "idpOidcDescription": "Een OpenID Connect identity provider configureren", - "idpCreatedDescription": "Identity provider succesvol aangemaakt", - "idpCreate": "Identity Provider aanmaken", - "idpCreateDescription": "Een nieuwe identiteitsprovider voor gebruikersauthenticatie configureren", - "idpSeeAll": "Zie alle identiteitsaanbieders", + "idpOidcDescription": "Een OpenID Connect identiteitsprovider configureren", + "idpCreatedDescription": "Identiteitsprovider succesvol aangemaakt", + "idpCreate": "Identiteitsprovider aanmaken", + "idpCreateDescription": "Een nieuwe identiteitsprovider voor authenticatie configureren", + "idpSeeAll": "Zie alle Identiteitsproviders", "idpSettingsDescription": "Configureer de basisinformatie voor uw identiteitsprovider", "idpDisplayName": "Een weergavenaam voor deze identiteitsprovider", "idpAutoProvisionUsers": "Auto Provisie Gebruikers", "idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.", - "licenseBadge": "Professioneel", + "licenseBadge": "EE", "idpType": "Type provider", "idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren", "idpOidcConfigure": "OAuth2/OIDC configuratie", "idpOidcConfigureDescription": "Configureer de eindpunten van de OAuth2/OIDC provider en referenties", - "idpClientId": "Klant ID", - "idpClientIdDescription": "De OAuth2-client-ID van uw identiteitsprovider", - "idpClientSecret": "Clientgeheim", - "idpClientSecretDescription": "Het OAuth2-clientgeheim van je identiteitsprovider", + "idpClientId": "Client ID", + "idpClientIdDescription": "De OAuth2 client ID van uw identiteitsprovider", + "idpClientSecret": "Client Secret", + "idpClientSecretDescription": "Het OAuth2 Client Secret van je identiteitsprovider", "idpAuthUrl": "URL autorisatie", "idpAuthUrlDescription": "De URL voor autorisatie OAuth2", "idpTokenUrl": "URL token", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Verbonden", "idpErrorConnectingTo": "Er was een probleem bij het verbinden met {name}. Neem contact op met uw beheerder.", "idpErrorNotFound": "IdP niet gevonden", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Ongeldige uitnodiging", "inviteInvalidDescription": "Uitnodigingslink is ongeldig.", "inviteErrorWrongUser": "Uitnodiging is niet voor deze gebruiker", @@ -924,7 +940,7 @@ "inviteErrorExpired": "De uitnodiging is mogelijk verlopen", "inviteErrorRevoked": "De uitnodiging is mogelijk ingetrokken", "inviteErrorTypo": "Er kan een typefout zijn in de uitnodigingslink", - "pangolinSetup": "Setup - Pangolin", + "pangolinSetup": "Instellen - Pangolin", "orgNameRequired": "Organisatienaam is vereist", "orgIdRequired": "Organisatie-ID is vereist", "orgErrorCreate": "Fout opgetreden tijdens het aanmaken org", @@ -994,12 +1010,12 @@ "actionGetUser": "Gebruiker ophalen", "actionGetOrgUser": "Krijg organisatie-gebruiker", "actionListOrgDomains": "Lijst organisatie domeinen", - "actionCreateSite": "Site maken", + "actionCreateSite": "Site aanmaken", "actionDeleteSite": "Site verwijderen", "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", "actionApplyBlueprint": "Blauwdruk toepassen", - "setupToken": "Setup Token", + "setupToken": "Instel Token", "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", "setupTokenRequired": "Setup-token is vereist", "actionUpdateSite": "Site bijwerken", @@ -1083,7 +1099,6 @@ "navbar": "Navigatiemenu", "navbarDescription": "Hoofd navigatie menu voor de applicatie", "navbarDocsLink": "Documentatie", - "commercialEdition": "Commerciële editie", "otpErrorEnable": "Kan 2FA niet inschakelen", "otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA", "otpSetupCheckCode": "Voer een 6-cijferige code in", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Alle gebruikers", "sidebarIdentityProviders": "Identiteit aanbieders", "sidebarLicense": "Licentie", - "sidebarClients": "Clients (Bèta)", + "sidebarClients": "Clients", "sidebarDomains": "Domeinen", "enableDockerSocket": "Schakel Docker Blauwdruk in", "enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.", @@ -1147,7 +1162,7 @@ "viewDockerContainers": "Bekijk Docker containers", "containersIn": "Containers in {siteName}", "selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.", - "containerName": "Naam", + "containerName": "naam", "containerImage": "Afbeelding", "containerState": "Provincie", "containerNetworks": "Netwerken", @@ -1255,8 +1270,50 @@ "domainPickerOrganizationDomains": "Organisatiedomeinen", "domainPickerProvidedDomains": "Aangeboden domeinen", "domainPickerSubdomain": "Subdomein: {subdomain}", - "domainPickerNamespace": "Namespace: {namespace}", + "domainPickerNamespace": "Naamruimte: {namespace}", "domainPickerShowMore": "Meer weergeven", + "regionSelectorTitle": "Selecteer Regio", + "regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.", + "regionSelectorPlaceholder": "Kies een regio", + "regionSelectorComingSoon": "Komt binnenkort", + "billingLoadingSubscription": "Abonnement laden...", + "billingFreeTier": "Gratis Niveau", + "billingWarningOverLimit": "Waarschuwing: U hebt een of meer gebruikslimieten overschreden. Uw sites maken geen verbinding totdat u uw abonnement aanpast of uw gebruik aanpast.", + "billingUsageLimitsOverview": "Overzicht gebruikslimieten", + "billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@fossorial.io.", + "billingDataUsage": "Gegevensgebruik", + "billingOnlineTime": "Site Online Tijd", + "billingUsers": "Actieve Gebruikers", + "billingDomains": "Actieve Domeinen", + "billingRemoteExitNodes": "Actieve Zelfgehoste Nodes", + "billingNoLimitConfigured": "Geen limiet ingesteld", + "billingEstimatedPeriod": "Geschatte Facturatie Periode", + "billingIncludedUsage": "Opgenomen Gebruik", + "billingIncludedUsageDescription": "Gebruik inbegrepen in uw huidige abonnementsplan", + "billingFreeTierIncludedUsage": "Gratis niveau gebruikstoelagen", + "billingIncluded": "inbegrepen", + "billingEstimatedTotal": "Geschat Totaal:", + "billingNotes": "Notities", + "billingEstimateNote": "Dit is een schatting gebaseerd op uw huidige gebruik.", + "billingActualChargesMayVary": "Facturering kan variëren.", + "billingBilledAtEnd": "U wordt aan het einde van de factureringsperiode gefactureerd.", + "billingModifySubscription": "Abonnementsaanpassing", + "billingStartSubscription": "Abonnement Starten", + "billingRecurringCharge": "Terugkerende Kosten", + "billingManageSubscriptionSettings": "Beheer uw 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", + "billingFailedToGetCheckoutUrl": "Niet gelukt om checkout URL te krijgen", + "billingPleaseTryAgainLater": "Probeer het later opnieuw.", + "billingCheckoutError": "Checkout Fout", + "billingFailedToGetPortalUrl": "Niet gelukt om portal URL te krijgen", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.", "twoFactor": "Tweestapsverificatie", "adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.", - "continueToApplication": "Doorgaan naar de applicatie", "securityKeyAdd": "Beveiligingssleutel toevoegen", "securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren", "securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS-wijzigingen kunnen enige tijd duren om over het internet te worden verspreid. Dit kan enkele minuten tot 48 uur duren, afhankelijk van je DNS-provider en TTL-instellingen.", "resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen", "resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen", + "billingPricingCalculatorLink": "Prijs Calculator", "signUpTerms": { "IAgreeToThe": "Ik ga akkoord met de", "termsOfService": "servicevoorwaarden", @@ -1349,7 +1406,7 @@ "olmId": "Olm ID", "olmSecretKey": "Olm Geheime Sleutel", "clientCredentialsSave": "Uw referenties opslaan", - "clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer deze naar een veilige plek.", + "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 ", "clientUpdatedDescription": "De client is bijgewerkt.", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Externe Proxy Ingeschakeld", "addNewTarget": "Voeg nieuw doelwit toe", "targetsList": "Lijst met doelen", + "advancedMode": "Geavanceerde modus", "targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden", + "healthCheckHealthy": "Gezond", + "healthCheckUnhealthy": "Ongezond", + "healthCheckUnknown": "Onbekend", + "healthCheck": "Gezondheidscontrole", + "configureHealthCheck": "Configureer Gezondheidscontrole", + "configureHealthCheckDescription": "Stel gezondheid monitor voor {target} in", + "enableHealthChecks": "Inschakelen Gezondheidscontroles", + "enableHealthChecksDescription": "Controleer de gezondheid van dit doel. U kunt een ander eindpunt monitoren dan het doel indien vereist.", + "healthScheme": "Methode", + "healthSelectScheme": "Selecteer methode", + "healthCheckPath": "Pad", + "healthHostname": "IP / Hostnaam", + "healthPort": "Poort", + "healthCheckPathDescription": "Het pad om de gezondheid status te controleren.", + "healthyIntervalSeconds": "Gezonde Interval", + "unhealthyIntervalSeconds": "Ongezonde Interval", + "IntervalSeconds": "Gezonde Interval", + "timeoutSeconds": "Timeout", + "timeIsInSeconds": "Tijd is in seconden", + "retryAttempts": "Herhaal Pogingen", + "expectedResponseCodes": "Verwachte Reactiecodes", + "expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.", + "customHeaders": "Aangepaste headers", + "customHeadersDescription": "Kopregeleinde: Header-Naam: waarde", + "headersValidationError": "Headers moeten in het formaat zijn: Header-Naam: waarde.", + "saveHealthCheck": "Opslaan Gezondheidscontrole", + "healthCheckSaved": "Gezondheidscontrole Opgeslagen", + "healthCheckSavedDescription": "Gezondheidscontrole configuratie succesvol opgeslagen", + "healthCheckError": "Gezondheidscontrole Fout", + "healthCheckErrorDescription": "Er is een fout opgetreden bij het opslaan van de configuratie van de gezondheidscontrole.", + "healthCheckPathRequired": "Gezondheidscontrole pad is vereist", + "healthCheckMethodRequired": "HTTP methode is vereist", + "healthCheckIntervalMin": "Controle interval moet minimaal 5 seconden zijn", + "healthCheckTimeoutMin": "Timeout moet minimaal 1 seconde zijn", + "healthCheckRetryMin": "Herhaal pogingen moet minimaal 1 zijn", "httpMethod": "HTTP-methode", "selectHttpMethod": "Selecteer HTTP-methode", "domainPickerSubdomainLabel": "Subdomein", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Voer een subdomein in om te zoeken en te selecteren uit beschikbare gratis domeinen.", "domainPickerFreeDomains": "Gratis Domeinen", "domainPickerSearchForAvailableDomains": "Zoek naar beschikbare domeinen", + "domainPickerNotWorkSelfHosted": "Opmerking: Gratis aangeboden domeinen zijn momenteel niet beschikbaar voor zelf-gehoste instanties.", "resourceDomain": "Domein", "resourceEditDomain": "Domein bewerken", "siteName": "Site Naam", @@ -1463,6 +1557,72 @@ "autoLoginError": "Auto Login Fout", "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.", + "remoteExitNodeManageRemoteExitNodes": "Externe knooppunten", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nodes", + "searchRemoteExitNodes": "Knooppunten zoeken...", + "remoteExitNodeAdd": "Voeg node toe", + "remoteExitNodeErrorDelete": "Fout bij verwijderen node", + "remoteExitNodeQuestionRemove": "Weet u zeker dat u het node {selectedNode} uit de organisatie wilt verwijderen?", + "remoteExitNodeMessageRemove": "Eenmaal verwijderd, zal het knooppunt niet langer toegankelijk zijn.", + "remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.", + "remoteExitNodeConfirmDelete": "Bevestig verwijderen node", + "remoteExitNodeDelete": "Knoop verwijderen", + "sidebarRemoteExitNodes": "Externe knooppunten", + "remoteExitNodeCreate": { + "title": "Maak node", + "description": "Maak een nieuwe node aan om uw 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.", + "adopt": { + "title": "Adopteer Node", + "description": "Kies dit als u al de referenties voor deze node heeft" + }, + "generate": { + "title": "Genereer Sleutels", + "description": "Kies dit als u nieuwe sleutels voor het knooppunt wilt genereren" + } + }, + "adopt": { + "title": "Adopteer Bestaande Node", + "description": "Voer de referenties in van het bestaande knooppunt dat u wilt adopteren", + "nodeIdLabel": "Knooppunt ID", + "nodeIdDescription": "De ID van het knooppunt dat u wilt adopteren", + "secretLabel": "Geheim", + "secretDescription": "De geheime sleutel van de bestaande node", + "submitButton": "Knooppunt adopteren" + }, + "generate": { + "title": "Gegeneerde Inloggegevens", + "description": "Gebruik deze gegenereerde inloggegevens om uw node te configureren", + "nodeIdTitle": "Knooppunt ID", + "secretTitle": "Geheim", + "saveCredentialsTitle": "Voeg Inloggegevens toe aan Config", + "saveCredentialsDescription": "Voeg deze inloggegevens toe aan uw zelf-gehoste Pangolin-node configuratiebestand om de verbinding te voltooien.", + "submitButton": "Maak node" + }, + "validation": { + "adoptRequired": "Node ID en Secret zijn verplicht bij het overnemen van een bestaand knooppunt" + }, + "errors": { + "loadDefaultsFailed": "Niet gelukt om standaarden te laden", + "defaultsNotLoaded": "Standaarden niet geladen", + "createFailed": "Fout bij het maken van node" + }, + "success": { + "created": "Node succesvol aangemaakt" + } + }, + "remoteExitNodeSelection": "Knooppunt selectie", + "remoteExitNodeSelectionDescription": "Selecteer een node om het verkeer door te leiden voor deze lokale site", + "remoteExitNodeRequired": "Een node moet worden geselecteerd voor lokale sites", + "noRemoteExitNodesAvailable": "Geen knooppunten beschikbaar", + "noRemoteExitNodesAvailableDescription": "Er zijn geen knooppunten beschikbaar voor deze organisatie. Maak eerst een knooppunt aan om lokale sites te gebruiken.", + "exitNode": "Exit Node", + "country": "Land", + "rulesMatchCountry": "Momenteel gebaseerd op bron IP", "managedSelfHosted": { "title": "Beheerde Self-Hosted", "description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Internationaal Domein Gedetecteerd", "willbestoredas": "Zal worden opgeslagen als:", + "roleMappingDescription": "Bepaal hoe rollen worden toegewezen aan gebruikers wanneer ze inloggen wanneer Auto Provision is ingeschakeld.", + "selectRole": "Selecteer een rol", + "roleMappingExpression": "Expressie", + "selectRolePlaceholder": "Kies een rol", + "selectRoleDescription": "Selecteer een rol om toe te wijzen aan alle gebruikers van deze identiteitsprovider", + "roleMappingExpressionDescription": "Voer een JMESPath expressie in om rolinformatie van de ID-token te extraheren", + "idpTenantIdRequired": "Tenant ID is vereist", + "invalidValue": "Ongeldige waarde", + "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", + "idpAzureConfiguration": "Azure Entra ID configuratie", + "idpAzureConfigurationDescription": "Configureer uw Azure Entra ID OAuth2 referenties", + "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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Google OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Aangepaste headers", - "headersValidationError": "Headers moeten in het formaat zijn: Header-Naam: waarde.", + "subnet": "Subnet", + "subnetDescription": "Het subnet van de netwerkconfiguratie van deze organisatie.", + "authPage": "Authenticatie pagina", + "authPageDescription": "De autorisatiepagina voor uw organisatie configureren", + "authPageDomain": "Authenticatie pagina domein", + "noDomainSet": "Geen domein ingesteld", + "changeDomain": "Domein wijzigen", + "selectDomain": "Domein selecteren", + "restartCertificate": "Certificaat opnieuw starten", + "editAuthPageDomain": "Authenticatiepagina domein bewerken", + "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", + "selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie", "domainPickerProvidedDomain": "Opgegeven domein", "domainPickerFreeProvidedDomain": "Gratis verstrekt domein", "domainPickerVerified": "Geverifieerd", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Een abonnement is vereist om deze functie te gebruiken.", + "idpDisabled": "Identiteitsaanbieders zijn uitgeschakeld.", + "orgAuthPageDisabled": "Pagina voor organisatie-authenticatie is uitgeschakeld.", + "domainRestartedDescription": "Domeinverificatie met succes opnieuw gestart", "resourceAddEntrypointsEditFile": "Bestand bewerken: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Bestand bewerken: docker-compose.yml" + "resourceExposePortsEditFile": "Bestand bewerken: docker-compose.yml", + "emailVerificationRequired": "E-mail verificatie is vereist. Log opnieuw in via {dashboardUrl}/auth/login voltooide deze stap. Kom daarna hier terug.", + "twoFactorSetupRequired": "Tweestapsverificatie instellen is vereist. Log opnieuw in via {dashboardUrl}/auth/login voltooide deze stap. Kom daarna hier terug.", + "authPageErrorUpdateMessage": "Er is een fout opgetreden bij het bijwerken van de instellingen van de auth-pagina", + "authPageUpdated": "Auth-pagina succesvol bijgewerkt", + "healthCheckNotAvailable": "Lokaal", + "rewritePath": "Herschrijf Pad", + "rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt.", + "continueToApplication": "Doorgaan naar applicatie", + "checkingInvite": "Uitnodiging controleren", + "setResourceHeaderAuth": "stelResourceHeaderAuth", + "resourceHeaderAuthRemove": "Auth koptekst verwijderen", + "resourceHeaderAuthRemoveDescription": "Koptekst authenticatie succesvol verwijderd.", + "resourceErrorHeaderAuthRemove": "Kan Header-authenticatie niet verwijderen", + "resourceErrorHeaderAuthRemoveDescription": "Kon header authenticatie niet verwijderen voor de bron.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Kan Header Authenticatie niet instellen", + "resourceErrorHeaderAuthSetupDescription": "Kan geen header authenticatie instellen voor de bron.", + "resourceHeaderAuthSetup": "Header Authenticatie set succesvol", + "resourceHeaderAuthSetupDescription": "Header authenticatie is met succes ingesteld.", + "resourceHeaderAuthSetupTitle": "Header Authenticatie instellen", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Header Authenticatie instellen", + "actionSetResourceHeaderAuth": "Header Authenticatie instellen", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Prioriteit", + "priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 4fe382e1..834fd4be 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -96,7 +96,7 @@ "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 tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Zobacz wszystkie witryny", "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", "siteNewtCredentials": "Aktualne dane logowania", @@ -168,6 +168,9 @@ "siteSelect": "Wybierz witrynę", "siteSearch": "Szukaj witryny", "siteNotFound": "Nie znaleziono witryny.", + "selectCountry": "Wybierz kraj", + "searchCountries": "Szukaj krajów...", + "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", @@ -465,7 +468,10 @@ "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 (https)", + "proxyEnableSSL": "Włącz SSL", + "proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.", + "target": "Target", + "configureTarget": "Konfiguruj Targety", "targetErrorFetch": "Nie udało się pobrać celów", "targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów", "siteErrorFetch": "Nie udało się pobrać zasobu", @@ -492,7 +498,7 @@ "targetTlsSettings": "Konfiguracja bezpiecznego połączenia", "targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu", "targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS", - "targetTlsSni": "Nazwa serwera TLS (SNI)", + "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", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", "methodSelect": "Wybierz metodę", "targetSubmit": "Dodaj cel", - "targetNoOne": "Brak celów. Dodaj cel używając formularza.", + "targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.", "targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.", "targetsSubmit": "Zapisz cele", + "addTarget": "Dodaj cel", + "targetErrorInvalidIp": "Nieprawidłowy adres IP", + "targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta", + "targetErrorInvalidPort": "Nieprawidłowy port", + "targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu", + "targetErrorNoSite": "Nie wybrano witryny", + "targetErrorNoSiteDescription": "Wybierz witrynę docelową", + "targetCreated": "Cel utworzony", + "targetCreatedDescription": "Cel został utworzony pomyślnie", + "targetErrorCreate": "Nie udało się utworzyć celu", + "targetErrorCreateDescription": "Wystąpił błąd podczas tworzenia celu", + "save": "Zapisz", "proxyAdditional": "Dodatkowe ustawienia proxy", "proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy", "proxyCustomHeader": "Niestandardowy nagłówek hosta", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Administrator serwera - Pangolin", "licenseTierProfessional": "Licencja Professional", "licenseTierEnterprise": "Licencja Enterprise", - "licenseTierCommercial": "Licencja handlowa", + "licenseTierPersonal": "Personal License", "licensed": "Licencjonowany", "yes": "Tak", "no": "Nie", @@ -747,7 +765,7 @@ "idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości", "idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników", "idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.", - "licenseBadge": "Profesjonalny", + "licenseBadge": "EE", "idpType": "Typ dostawcy", "idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować", "idpOidcConfigure": "Konfiguracja OAuth2/OIDC", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Połączono", "idpErrorConnectingTo": "Wystąpił problem z połączeniem z {name}. Skontaktuj się z administratorem.", "idpErrorNotFound": "Nie znaleziono IdP", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Nieprawidłowe zaproszenie", "inviteInvalidDescription": "Link zapraszający jest nieprawidłowy.", "inviteErrorWrongUser": "Zaproszenie nie jest dla tego użytkownika", @@ -1083,7 +1099,6 @@ "navbar": "Menu nawigacyjne", "navbarDescription": "Główne menu nawigacyjne aplikacji", "navbarDocsLink": "Dokumentacja", - "commercialEdition": "Edycja komercyjna", "otpErrorEnable": "Nie można włączyć 2FA", "otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA", "otpSetupCheckCode": "Wprowadź 6-cyfrowy kod", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Wszyscy użytkownicy", "sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarLicense": "Licencja", - "sidebarClients": "Klienci (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domeny", "enableDockerSocket": "Włącz schemat dokera", "enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.", @@ -1155,7 +1170,7 @@ "containerLabels": "Etykiety", "containerLabelsCount": "{count, plural, one {# etykieta} few {# etykiety} many {# etykiet} other {# etykiet}}", "containerLabelsTitle": "Etykiety kontenera", - "containerLabelEmpty": "", + "containerLabelEmpty": "", "containerPorts": "Porty", "containerPortsMore": "+{count} więcej", "containerActions": "Akcje", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Subdomena: {subdomain}", "domainPickerNamespace": "Przestrzeń nazw: {namespace}", "domainPickerShowMore": "Pokaż więcej", + "regionSelectorTitle": "Wybierz region", + "regionSelectorInfo": "Wybór regionu pomaga nam zapewnić lepszą wydajność dla Twojej lokalizacji. Nie musisz być w tym samym regionie co Twój serwer.", + "regionSelectorPlaceholder": "Wybierz region", + "regionSelectorComingSoon": "Wkrótce dostępne", + "billingLoadingSubscription": "Ładowanie subskrypcji...", + "billingFreeTier": "Darmowy pakiet", + "billingWarningOverLimit": "Ostrzeżenie: Przekroczyłeś jeden lub więcej limitów użytkowania. Twoje witryny nie połączą się, dopóki nie zmienisz subskrypcji lub nie dostosujesz użytkowania.", + "billingUsageLimitsOverview": "Przegląd Limitów Użytkowania", + "billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@fossorial.io.", + "billingDataUsage": "Użycie danych", + "billingOnlineTime": "Czas Online Strony", + "billingUsers": "Aktywni użytkownicy", + "billingDomains": "Aktywne domeny", + "billingRemoteExitNodes": "Aktywne samodzielnie-hostowane węzły", + "billingNoLimitConfigured": "Nie skonfigurowano limitu", + "billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy", + "billingIncludedUsage": "Zawarte użycie", + "billingIncludedUsageDescription": "Użycie zawarte w obecnym planie subskrypcji", + "billingFreeTierIncludedUsage": "Limity użycia dla darmowego pakietu", + "billingIncluded": "zawarte", + "billingEstimatedTotal": "Szacowana Całkowita:", + "billingNotes": "Notatki", + "billingEstimateNote": "To jest szacunkowe, oparte na Twoim obecnym użyciu.", + "billingActualChargesMayVary": "Rzeczywiste opłaty mogą się różnić.", + "billingBilledAtEnd": "Zostaniesz obciążony na koniec okresu rozliczeniowego.", + "billingModifySubscription": "Modyfikuj Subskrypcję", + "billingStartSubscription": "Rozpocznij Subskrypcję", + "billingRecurringCharge": "Opłata Cyklowa", + "billingManageSubscriptionSettings": "Zarządzaj ustawieniami i preferencjami subskrypcji", + "billingNoActiveSubscription": "Nie masz aktywnej subskrypcji. Rozpocznij subskrypcję, aby zwiększyć limity użytkowania.", + "billingFailedToLoadSubscription": "Nie udało się załadować subskrypcji", + "billingFailedToLoadUsage": "Nie udało się załadować użycia", + "billingFailedToGetCheckoutUrl": "Nie udało się uzyskać adresu URL zakupu", + "billingPleaseTryAgainLater": "Spróbuj ponownie później.", + "billingCheckoutError": "Błąd przy kasie", + "billingFailedToGetPortalUrl": "Nie udało się uzyskać adresu URL portalu", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.", "twoFactor": "Uwierzytelnianie dwuskładnikowe", "adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.", - "continueToApplication": "Kontynuuj do aplikacji", "securityKeyAdd": "Dodaj klucz bezpieczeństwa", "securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa", "securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Zmiany DNS mogą zająć trochę czasu na rozpropagowanie się w Internecie. Może to potrwać od kilku minut do 48 godzin, w zależności od dostawcy DNS i ustawień TTL.", "resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP", "resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP", + "billingPricingCalculatorLink": "Kalkulator Cen", "signUpTerms": { "IAgreeToThe": "Zgadzam się z", "termsOfService": "warunkami usługi", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Zewnętrzny Proxy Włączony", "addNewTarget": "Dodaj nowy cel", "targetsList": "Lista celów", + "advancedMode": "Tryb zaawansowany", "targetErrorDuplicateTargetFound": "Znaleziono duplikat celu", + "healthCheckHealthy": "Zdrowy", + "healthCheckUnhealthy": "Niezdrowy", + "healthCheckUnknown": "Nieznany", + "healthCheck": "Kontrola Zdrowia", + "configureHealthCheck": "Skonfiguruj Kontrolę Zdrowia", + "configureHealthCheckDescription": "Skonfiguruj monitorowanie zdrowia dla {target}", + "enableHealthChecks": "Włącz Kontrole Zdrowia", + "enableHealthChecksDescription": "Monitoruj zdrowie tego celu. Możesz monitorować inny punkt końcowy niż docelowy w razie potrzeby.", + "healthScheme": "Metoda", + "healthSelectScheme": "Wybierz metodę", + "healthCheckPath": "Ścieżka", + "healthHostname": "IP / Nazwa hosta", + "healthPort": "Port", + "healthCheckPathDescription": "Ścieżka do sprawdzania stanu zdrowia.", + "healthyIntervalSeconds": "Interwał Zdrowy", + "unhealthyIntervalSeconds": "Interwał Niezdrowy", + "IntervalSeconds": "Interwał Zdrowy", + "timeoutSeconds": "Limit Czasu", + "timeIsInSeconds": "Czas w sekundach", + "retryAttempts": "Próby Ponowienia", + "expectedResponseCodes": "Oczekiwane Kody Odpowiedzi", + "expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.", + "customHeaders": "Niestandardowe nagłówki", + "customHeadersDescription": "Nagłówki oddzielone: Nazwa nagłówka: wartość", + "headersValidationError": "Nagłówki muszą być w formacie: Nazwa nagłówka: wartość.", + "saveHealthCheck": "Zapisz Kontrolę Zdrowia", + "healthCheckSaved": "Kontrola Zdrowia Zapisana", + "healthCheckSavedDescription": "Konfiguracja kontroli zdrowia została zapisana pomyślnie", + "healthCheckError": "Błąd Kontroli Zdrowia", + "healthCheckErrorDescription": "Wystąpił błąd podczas zapisywania konfiguracji kontroli zdrowia", + "healthCheckPathRequired": "Ścieżka kontroli zdrowia jest wymagana", + "healthCheckMethodRequired": "Metoda HTTP jest wymagana", + "healthCheckIntervalMin": "Interwał sprawdzania musi wynosić co najmniej 5 sekund", + "healthCheckTimeoutMin": "Limit czasu musi wynosić co najmniej 1 sekundę", + "healthCheckRetryMin": "Liczba prób ponowienia musi wynosić co najmniej 1", "httpMethod": "Metoda HTTP", "selectHttpMethod": "Wybierz metodę HTTP", "domainPickerSubdomainLabel": "Poddomena", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Wprowadź poddomenę, aby wyszukać i wybrać z dostępnych darmowych domen.", "domainPickerFreeDomains": "Darmowe domeny", "domainPickerSearchForAvailableDomains": "Szukaj dostępnych domen", + "domainPickerNotWorkSelfHosted": "Uwaga: Darmowe domeny nie są obecnie dostępne dla instancji samodzielnie-hostowanych.", "resourceDomain": "Domena", "resourceEditDomain": "Edytuj domenę", "siteName": "Nazwa strony", @@ -1463,6 +1557,72 @@ "autoLoginError": "Błąd automatycznego logowania", "autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.", "autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.", + "remoteExitNodeManageRemoteExitNodes": "Zdalne węzły", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Węzły", + "searchRemoteExitNodes": "Szukaj węzłów...", + "remoteExitNodeAdd": "Dodaj węzeł", + "remoteExitNodeErrorDelete": "Błąd podczas usuwania węzła", + "remoteExitNodeQuestionRemove": "Czy na pewno chcesz usunąć węzeł {selectedNode} z organizacji?", + "remoteExitNodeMessageRemove": "Po usunięciu, węzeł nie będzie już dostępny.", + "remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.", + "remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła", + "remoteExitNodeDelete": "Usuń węzeł", + "sidebarRemoteExitNodes": "Zdalne węzły", + "remoteExitNodeCreate": { + "title": "Utwórz węzeł", + "description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią", + "viewAllButton": "Zobacz wszystkie węzły", + "strategy": { + "title": "Strategia Tworzenia", + "description": "Wybierz to, aby ręcznie skonfigurować węzeł lub wygenerować nowe poświadczenia.", + "adopt": { + "title": "Zaadoptuj Węzeł", + "description": "Wybierz to, jeśli masz już dane logowania dla węzła." + }, + "generate": { + "title": "Generuj Klucze", + "description": "Wybierz to, jeśli chcesz wygenerować nowe klucze dla węzła" + } + }, + "adopt": { + "title": "Zaadoptuj Istniejący Węzeł", + "description": "Wprowadź dane logowania istniejącego węzła, który chcesz przyjąć", + "nodeIdLabel": "ID węzła", + "nodeIdDescription": "ID istniejącego węzła, który chcesz przyjąć", + "secretLabel": "Sekret", + "secretDescription": "Sekretny klucz istniejącego węzła", + "submitButton": "Przyjmij węzeł" + }, + "generate": { + "title": "Wygenerowane Poświadczenia", + "description": "Użyj tych danych logowania, aby skonfigurować węzeł", + "nodeIdTitle": "ID węzła", + "secretTitle": "Sekret", + "saveCredentialsTitle": "Dodaj Poświadczenia do Konfiguracji", + "saveCredentialsDescription": "Dodaj te poświadczenia do pliku konfiguracyjnego swojego samodzielnie-hostowanego węzła Pangolin, aby zakończyć połączenie.", + "submitButton": "Utwórz węzeł" + }, + "validation": { + "adoptRequired": "Identyfikator węzła i sekret są wymagane podczas przyjmowania istniejącego węzła" + }, + "errors": { + "loadDefaultsFailed": "Nie udało się załadować domyślnych ustawień", + "defaultsNotLoaded": "Domyślne ustawienia nie zostały załadowane", + "createFailed": "Nie udało się utworzyć węzła" + }, + "success": { + "created": "Węzeł utworzony pomyślnie" + } + }, + "remoteExitNodeSelection": "Wybór węzła", + "remoteExitNodeSelectionDescription": "Wybierz węzeł do przekierowania ruchu dla tej lokalnej witryny", + "remoteExitNodeRequired": "Węzeł musi być wybrany dla lokalnych witryn", + "noRemoteExitNodesAvailable": "Brak dostępnych węzłów", + "noRemoteExitNodesAvailableDescription": "Węzły nie są dostępne dla tej organizacji. Utwórz węzeł, aby używać lokalnych witryn.", + "exitNode": "Węzeł Wyjściowy", + "country": "Kraj", + "rulesMatchCountry": "Obecnie bazuje na adresie IP źródła", "managedSelfHosted": { "title": "Zarządzane Samodzielnie-Hostingowane", "description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Wykryto międzynarodową domenę", "willbestoredas": "Będą przechowywane jako:", + "roleMappingDescription": "Określ jak role są przypisywane do użytkowników podczas logowania się, gdy automatyczne świadczenie jest włączone.", + "selectRole": "Wybierz rolę", + "roleMappingExpression": "Wyrażenie", + "selectRolePlaceholder": "Wybierz rolę", + "selectRoleDescription": "Wybierz rolę do przypisania wszystkim użytkownikom od tego dostawcy tożsamości", + "roleMappingExpressionDescription": "Wprowadź wyrażenie JMESŚcieżki, aby wyodrębnić informacje o roli z tokenu ID", + "idpTenantIdRequired": "ID lokatora jest wymagane", + "invalidValue": "Nieprawidłowa wartość", + "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", + "idpAzureConfiguration": "Konfiguracja Azure Entra ID", + "idpAzureConfigurationDescription": "Skonfiguruj swoje dane logowania OAuth2 Azure Entra", + "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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Dostawca Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Niestandardowe nagłówki", - "headersValidationError": "Nagłówki muszą być w formacie: Nazwa nagłówka: wartość.", + "subnet": "Podsieć", + "subnetDescription": "Podsieć dla konfiguracji sieci tej organizacji.", + "authPage": "Strona uwierzytelniania", + "authPageDescription": "Skonfiguruj stronę uwierzytelniania dla swojej organizacji", + "authPageDomain": "Domena strony uwierzytelniania", + "noDomainSet": "Nie ustawiono domeny", + "changeDomain": "Zmień domenę", + "selectDomain": "Wybierz domenę", + "restartCertificate": "Uruchom ponownie certyfikat", + "editAuthPageDomain": "Edytuj domenę strony uwierzytelniania", + "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", + "selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji", "domainPickerProvidedDomain": "Dostarczona domena", "domainPickerFreeProvidedDomain": "Darmowa oferowana domena", "domainPickerVerified": "Zweryfikowano", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" nie może być poprawne dla {domain}.", "domainPickerSubdomainSanitized": "Poddomena oczyszczona", "domainPickerSubdomainCorrected": "\"{sub}\" został skorygowany do \"{sanitized}\"", + "orgAuthSignInTitle": "Zaloguj się do swojej 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", + "subscriptionRequiredToUse": "Do korzystania z tej funkcji wymagana jest subskrypcja.", + "idpDisabled": "Dostawcy tożsamości są wyłączeni", + "orgAuthPageDisabled": "Strona autoryzacji organizacji jest wyłączona.", + "domainRestartedDescription": "Weryfikacja domeny zrestartowana pomyślnie", "resourceAddEntrypointsEditFile": "Edytuj plik: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Edytuj plik: docker-compose.yml" + "resourceExposePortsEditFile": "Edytuj plik: docker-compose.yml", + "emailVerificationRequired": "Weryfikacja adresu e-mail jest wymagana. Zaloguj się ponownie przez {dashboardUrl}/auth/login zakończył ten krok. Następnie wróć tutaj.", + "twoFactorSetupRequired": "Konfiguracja uwierzytelniania dwuskładnikowego jest wymagana. Zaloguj się ponownie przez {dashboardUrl}/auth/login dokończ ten krok. Następnie wróć tutaj.", + "authPageErrorUpdateMessage": "Wystąpił błąd podczas aktualizacji ustawień strony uwierzytelniania", + "authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana", + "healthCheckNotAvailable": "Lokalny", + "rewritePath": "Przepis Ścieżki", + "rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu.", + "continueToApplication": "Kontynuuj do aplikacji", + "checkingInvite": "Sprawdzanie zaproszenia", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Usuń autoryzację nagłówka", + "resourceHeaderAuthRemoveDescription": "Uwierzytelnianie nagłówka zostało pomyślnie usunięte.", + "resourceErrorHeaderAuthRemove": "Nie udało się usunąć uwierzytelniania nagłówka", + "resourceErrorHeaderAuthRemoveDescription": "Nie można usunąć uwierzytelniania nagłówka zasobu.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Nie udało się ustawić uwierzytelniania nagłówka", + "resourceErrorHeaderAuthSetupDescription": "Nie można ustawić uwierzytelniania nagłówka dla zasobu.", + "resourceHeaderAuthSetup": "Uwierzytelnianie nagłówka ustawione pomyślnie", + "resourceHeaderAuthSetupDescription": "Uwierzytelnianie nagłówka zostało ustawione.", + "resourceHeaderAuthSetupTitle": "Ustaw uwierzytelnianie nagłówka", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Ustaw uwierzytelnianie nagłówka", + "actionSetResourceHeaderAuth": "Ustaw uwierzytelnianie nagłówka", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Priorytet", + "priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 9fd73d49..0a93a357 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -8,25 +8,25 @@ "orgId": "ID da organização", "setupIdentifierMessage": "Este é o identificador exclusivo para sua organização. Isso é separado do nome de exibição.", "setupErrorIdentifier": "O ID da organização já existe. Por favor, escolha um diferente.", - "componentsErrorNoMemberCreate": "Você não é atualmente um membro de nenhuma organização. Crie uma organização para começar.", - "componentsErrorNoMember": "Você não é atualmente um membro de nenhuma organização.", + "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.", "welcome": "Bem-vindo ao Pangolin", "welcomeTo": "Bem-vindo ao", "componentsCreateOrg": "Criar uma organização", - "componentsMember": "Você é membro de {count, plural, =0 {nenhuma organização} one {uma organização} other {# organizações}}.", + "componentsMember": "É membro de {count, plural, =0 {nenhuma organização} one {uma organização} other {# organizações}}.", "componentsInvalidKey": "Chaves de licença inválidas ou expiradas detectadas. Siga os termos da licença para continuar usando todos os recursos.", - "dismiss": "Descartar", + "dismiss": "Rejeitar", "componentsLicenseViolation": "Violação de Licença: Este servidor está usando sites {usedSites} que excedem o limite licenciado de sites {maxSites} . Siga os termos da licença para continuar usando todos os recursos.", "componentsSupporterMessage": "Obrigado por apoiar o Pangolin como um {tier}!", - "inviteErrorNotValid": "Desculpe, mas parece que o convite que você está tentando acessar não foi aceito ou não é mais válido.", - "inviteErrorUser": "Lamentamos, mas parece que o convite que você está tentando acessar não é para este usuário.", - "inviteLoginUser": "Verifique se você está logado como o usuário correto.", - "inviteErrorNoUser": "Desculpe, mas parece que o convite que você está tentando acessar não é para um usuário que existe.", + "inviteErrorNotValid": "Desculpe, mas parece que o convite que está a tentar aceder não foi aceito ou não é mais válido.", + "inviteErrorUser": "Lamentamos, mas parece que o convite que está a tentar aceder não é para este utilizador.", + "inviteLoginUser": "Verifique se você está logado como o utilizador correto.", + "inviteErrorNoUser": "Desculpe, mas parece que o convite que está a tentar aceder não é para um utilizador que existe.", "inviteCreateUser": "Por favor, crie uma conta primeiro.", - "goHome": "Ir para casa", - "inviteLogInOtherUser": "Fazer login como um usuário diferente", + "goHome": "Voltar ao inicio", + "inviteLogInOtherUser": "Fazer login como um utilizador diferente", "createAnAccount": "Crie uma conta", - "inviteNotAccepted": "Convite não aceito", + "inviteNotAccepted": "Convite não aceite", "authCreateAccount": "Crie uma conta para começar", "authNoAccount": "Não possui uma conta?", "email": "e-mail", @@ -34,23 +34,23 @@ "confirmPassword": "Confirmar senha", "createAccount": "Criar conta", "viewSettings": "Visualizar configurações", - "delete": "excluir", + "delete": "apagar", "name": "Nome:", "online": "Disponível", "offline": "Desconectado", "site": "site", - "dataIn": "Dados em", + "dataIn": "Dados de entrada", "dataOut": "Dados de saída", "connectionType": "Tipo de conexão", "tunnelType": "Tipo de túnel", "local": "Localização", "edit": "Alterar", - "siteConfirmDelete": "Confirmar exclusão do site", + "siteConfirmDelete": "Confirmar que pretende apagar o site", "siteDelete": "Excluir site", "siteMessageRemove": "Uma vez removido, o site não estará mais acessível. Todos os recursos e alvos associados ao site também serão removidos.", "siteMessageConfirm": "Para confirmar, por favor, digite o nome do site abaixo.", "siteQuestionRemove": "Você tem certeza que deseja remover o site {selectedSite} da organização?", - "siteManageSites": "Gerenciar sites", + "siteManageSites": "Gerir sites", "siteDescription": "Permitir conectividade à sua rede através de túneis seguros", "siteCreate": "Criar site", "siteCreateDescription2": "Siga os passos abaixo para criar e conectar um novo site", @@ -79,10 +79,10 @@ "operatingSystem": "Sistema operacional", "commands": "Comandos", "recommended": "Recomendados", - "siteNewtDescription": "Para a melhor experiência do usuário, utilize Novo. Ele usa o WireGuard sob o capuz e permite que você aborde seus recursos privados através dos endereços LAN em sua rede privada do painel do Pangolin.", + "siteNewtDescription": "Para a melhor experiência do utilizador, utilize Novo. Ele usa o WireGuard sob o capuz e permite que você aborde seus recursos privados através dos endereços LAN em sua rede privada do painel do Pangolin.", "siteRunsInDocker": "Executa no Docker", "siteRunsInShell": "Executa na shell no macOS, Linux e Windows", - "siteErrorDelete": "Erro ao excluir site", + "siteErrorDelete": "Erro ao apagar site", "siteErrorUpdate": "Falha ao atualizar site", "siteErrorUpdateDescription": "Ocorreu um erro ao atualizar o site.", "siteUpdated": "Site atualizado", @@ -96,7 +96,7 @@ "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 tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Ver todos os sites", "siteTunnelDescription": "Determine como você deseja se conectar ao seu site", "siteNewtCredentials": "Credenciais Novas", @@ -105,12 +105,12 @@ "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": "Gerenciar links de compartilhamento", + "shareTitle": "Gerir links partilhados", "shareDescription": "Criar links compartilháveis para conceder acesso temporário ou permanente aos seus recursos", "shareSearch": "Pesquisar links de compartilhamento...", "shareCreate": "Criar Link de Compartilhamento", - "shareErrorDelete": "Falha ao excluir o link", - "shareErrorDeleteMessage": "Ocorreu um erro ao excluir o link", + "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.", @@ -127,13 +127,13 @@ "shareErrorFetchResourceDescription": "Ocorreu um erro ao obter os recursos", "shareErrorCreate": "Falha ao criar link de compartilhamento", "shareErrorCreateDescription": "Ocorreu um erro ao criar o link de compartilhamento", - "shareCreateDescription": "Qualquer um com este link pode acessar o recurso", + "shareCreateDescription": "Qualquer um com este link pode aceder o recurso", "shareTitleOptional": "Título (opcional)", "expireIn": "Expira em", "neverExpire": "Nunca expirar", - "shareExpireDescription": "Tempo de expiração é quanto tempo o link será utilizável e oferecerá acesso ao recurso. Após este tempo, o link não funcionará mais, e os usuários que usaram este link perderão acesso ao recurso.", + "shareExpireDescription": "Tempo de expiração é quanto tempo o link será utilizável e oferecerá acesso ao recurso. Após este tempo, o link não funcionará mais, e os utilizadores que usaram este link perderão acesso ao recurso.", "shareSeeOnce": "Você só poderá ver este link uma vez. Certifique-se de copiá-lo.", - "shareAccessHint": "Qualquer um com este link pode acessar o recurso. Compartilhe com cuidado.", + "shareAccessHint": "Qualquer um com este link pode aceder o recurso. Compartilhe com cuidado.", "shareTokenUsage": "Ver Uso do Token de Acesso", "createLink": "Criar Link", "resourcesNotFound": "Nenhum recurso encontrado", @@ -145,11 +145,11 @@ "expires": "Expira", "never": "nunca", "shareErrorSelectResource": "Por favor, selecione um recurso", - "resourceTitle": "Gerenciar Recursos", + "resourceTitle": "Gerir Recursos", "resourceDescription": "Crie proxies seguros para seus aplicativos privados", "resourcesSearch": "Procurar recursos...", "resourceAdd": "Adicionar Recurso", - "resourceErrorDelte": "Erro ao excluir recurso", + "resourceErrorDelte": "Erro ao apagar recurso", "authentication": "Autenticação", "protected": "Protegido", "notProtected": "Não Protegido", @@ -168,9 +168,12 @@ "siteSelect": "Selecionar site", "siteSearch": "Procurar no site", "siteNotFound": "Nenhum site encontrado.", + "selectCountry": "Selecionar país", + "searchCountries": "Buscar países...", + "noCountryFound": "Nenhum país encontrado.", "siteSelectionDescription": "Este site fornecerá conectividade ao destino.", "resourceType": "Tipo de Recurso", - "resourceTypeDescription": "Determine como você deseja acessar seu recurso", + "resourceTypeDescription": "Determine como você deseja aceder seu recurso", "resourceHTTPSSettings": "Configurações de HTTPS", "resourceHTTPSSettingsDescription": "Configure como seu recurso será acessado por HTTPS", "domainType": "Tipo de domínio", @@ -192,7 +195,7 @@ "resourceBack": "Voltar aos recursos", "resourceGoTo": "Ir para o Recurso", "resourceDelete": "Excluir Recurso", - "resourceDeleteConfirm": "Confirmar exclusão de recurso", + "resourceDeleteConfirm": "Confirmar que pretende apagar o recurso", "visibility": "Visibilidade", "enabled": "Ativado", "disabled": "Desabilitado", @@ -208,14 +211,14 @@ "passToAuth": "Passar para Autenticação", "orgSettingsDescription": "Configurar as configurações gerais da sua organização", "orgGeneralSettings": "Configurações da organização", - "orgGeneralSettingsDescription": "Gerencie os detalhes e a configuração da sua organização", - "saveGeneralSettings": "Salvar configurações gerais", - "saveSettings": "Salvar Configurações", + "orgGeneralSettingsDescription": "Gerir os detalhes e a configuração da sua organização", + "saveGeneralSettings": "Guardar configurações gerais", + "saveSettings": "Guardar Configurações", "orgDangerZone": "Zona de Perigo", "orgDangerZoneDescription": "Uma vez que você exclui esta organização, não há volta. Por favor, tenha certeza.", "orgDelete": "Excluir Organização", - "orgDeleteConfirm": "Confirmar exclusão da organização", - "orgMessageRemove": "Esta ação é irreversível e excluirá todos os dados associados.", + "orgDeleteConfirm": "Confirmar que pretende apagar a organização", + "orgMessageRemove": "Esta ação é irreversível e apagará todos os dados associados.", "orgMessageConfirm": "Para confirmar, digite o nome da organização abaixo.", "orgQuestionRemove": "Tem certeza que deseja remover a organização {selectedOrg}?", "orgUpdated": "Organização atualizada", @@ -224,29 +227,29 @@ "orgErrorUpdateMessage": "Ocorreu um erro ao atualizar a organização.", "orgErrorFetch": "Falha ao buscar organizações", "orgErrorFetchMessage": "Ocorreu um erro ao listar suas organizações", - "orgErrorDelete": "Falha ao excluir organização", - "orgErrorDeleteMessage": "Ocorreu um erro ao excluir a organização.", + "orgErrorDelete": "Falha ao apagar organização", + "orgErrorDeleteMessage": "Ocorreu um erro ao apagar a organização.", "orgDeleted": "Organização excluída", "orgDeletedMessage": "A organização e seus dados foram excluídos.", "orgMissing": "ID da Organização Ausente", "orgMissingMessage": "Não é possível regenerar o convite sem um ID de organização.", - "accessUsersManage": "Gerenciar Usuários", - "accessUsersDescription": "Convidar usuários e adicioná-los a funções para gerenciar o acesso à sua organização", - "accessUsersSearch": "Procurar usuários...", + "accessUsersManage": "Gerir Utilizadores", + "accessUsersDescription": "Convidar utilizadores e adicioná-los a funções para gerir o acesso à sua organização", + "accessUsersSearch": "Procurar utilizadores...", "accessUserCreate": "Criar Usuário", - "accessUserRemove": "Remover usuário", + "accessUserRemove": "Remover utilizador", "username": "Usuário:", "identityProvider": "Provedor de Identidade", "role": "Funções", "nameRequired": "O nome é obrigatório", - "accessRolesManage": "Gerenciar Funções", - "accessRolesDescription": "Configurar funções para gerenciar o acesso à sua organização", + "accessRolesManage": "Gerir Funções", + "accessRolesDescription": "Configurar funções para gerir o acesso à sua organização", "accessRolesSearch": "Pesquisar funções...", "accessRolesAdd": "Adicionar função", "accessRoleDelete": "Excluir Papel", "description": "Descrição:", "inviteTitle": "Convites Abertos", - "inviteDescription": "Gerencie seus convites para outros usuários", + "inviteDescription": "Gerir seus convites para outros utilizadores", "inviteSearch": "Procurar convites...", "minutes": "minutos", "hours": "horas", @@ -264,7 +267,7 @@ "apiKeysGeneralSettings": "Permissões", "apiKeysGeneralSettingsDescription": "Determine o que esta chave API pode fazer", "apiKeysList": "Sua Chave API", - "apiKeysSave": "Salvar Sua Chave API", + "apiKeysSave": "Guardar Sua Chave API", "apiKeysSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-la para um local seguro.", "apiKeysInfo": "Sua chave API é:", "apiKeysConfirmCopy": "Eu copiei a chave API", @@ -277,33 +280,33 @@ "apiKeysPermissionsUpdatedDescription": "As permissões foram atualizadas.", "apiKeysPermissionsGeneralSettings": "Permissões", "apiKeysPermissionsGeneralSettingsDescription": "Determine o que esta chave API pode fazer", - "apiKeysPermissionsSave": "Salvar Permissões", + "apiKeysPermissionsSave": "Guardar Permissões", "apiKeysPermissionsTitle": "Permissões", "apiKeys": "Chaves API", "searchApiKeys": "Pesquisar chaves API...", "apiKeysAdd": "Gerar Chave API", - "apiKeysErrorDelete": "Erro ao excluir chave API", - "apiKeysErrorDeleteMessage": "Erro ao excluir chave API", + "apiKeysErrorDelete": "Erro ao apagar chave API", + "apiKeysErrorDeleteMessage": "Erro ao apagar chave API", "apiKeysQuestionRemove": "Tem certeza que deseja remover a chave API {selectedApiKey} da organização?", "apiKeysMessageRemove": "Uma vez removida, a chave API não poderá mais ser utilizada.", "apiKeysMessageConfirm": "Para confirmar, por favor digite o nome da chave API abaixo.", "apiKeysDeleteConfirm": "Confirmar Exclusão da Chave API", "apiKeysDelete": "Excluir Chave API", - "apiKeysManage": "Gerenciar Chaves API", + "apiKeysManage": "Gerir Chaves API", "apiKeysDescription": "As chaves API são usadas para autenticar com a API de integração", "apiKeysSettings": "Configurações de {apiKeyName}", - "userTitle": "Gerenciar Todos os Usuários", - "userDescription": "Visualizar e gerenciar todos os usuários no sistema", + "userTitle": "Gerir Todos os Utilizadores", + "userDescription": "Visualizar e gerir todos os utilizadores no sistema", "userAbount": "Sobre a Gestão de Usuário", - "userAbountDescription": "Esta tabela exibe todos os objetos root do usuário. Cada usuário pode pertencer a várias organizações. Remover um usuário de uma organização não exclui seu objeto de usuário raiz - ele permanecerá no sistema. Para remover completamente um usuário do sistema, você deve excluir seu objeto raiz usando a ação de excluir nesta tabela.", - "userServer": "Usuários do Servidor", - "userSearch": "Pesquisar usuários do servidor...", - "userErrorDelete": "Erro ao excluir usuário", + "userAbountDescription": "Esta tabela exibe todos os objetos root do utilizador. Cada utilizador pode pertencer a várias organizações. Remover um utilizador de uma organização não exclui seu objeto de utilizador raiz - ele permanecerá no sistema. Para remover completamente um utilizador do sistema, você deve apagar seu objeto raiz usando a ação de apagar nesta tabela.", + "userServer": "Utilizadores do Servidor", + "userSearch": "Pesquisar utilizadores do servidor...", + "userErrorDelete": "Erro ao apagar utilizador", "userDeleteConfirm": "Confirmar Exclusão do Usuário", - "userDeleteServer": "Excluir usuário do servidor", - "userMessageRemove": "O usuário será removido de todas as organizações e será completamente removido do servidor.", - "userMessageConfirm": "Para confirmar, por favor digite o nome do usuário abaixo.", - "userQuestionRemove": "Tem certeza que deseja excluir o {selectedUser} permanentemente do servidor?", + "userDeleteServer": "Excluir utilizador do servidor", + "userMessageRemove": "O utilizador será removido de todas as organizações e será completamente removido do servidor.", + "userMessageConfirm": "Para confirmar, por favor digite o nome do utilizador abaixo.", + "userQuestionRemove": "Tem certeza que deseja apagar o {selectedUser} permanentemente do servidor?", "licenseKey": "Chave de Licença", "valid": "Válido", "numberOfSites": "Número de sites", @@ -314,8 +317,8 @@ "licenseTermsAgree": "Você deve concordar com os termos da licença", "licenseErrorKeyLoad": "Falha ao carregar chaves de licença", "licenseErrorKeyLoadDescription": "Ocorreu um erro ao carregar a chave da licença.", - "licenseErrorKeyDelete": "Falha ao excluir chave de licença", - "licenseErrorKeyDeleteDescription": "Ocorreu um erro ao excluir a chave de licença.", + "licenseErrorKeyDelete": "Falha ao apagar chave de licença", + "licenseErrorKeyDeleteDescription": "Ocorreu um erro ao apagar a chave de licença.", "licenseKeyDeleted": "Chave da licença excluída", "licenseKeyDeletedDescription": "A chave da licença foi excluída.", "licenseErrorKeyActivate": "Falha ao ativar a chave de licença", @@ -336,13 +339,13 @@ "fossorialLicense": "Ver Termos e Condições de Assinatura e Licença Fossorial", "licenseMessageRemove": "Isto irá remover a chave da licença e todas as permissões associadas concedidas por ela.", "licenseMessageConfirm": "Para confirmar, por favor, digite a chave de licença abaixo.", - "licenseQuestionRemove": "Tem certeza que deseja excluir a chave de licença {selectedKey}?", + "licenseQuestionRemove": "Tem certeza que deseja apagar a chave de licença {selectedKey}?", "licenseKeyDelete": "Excluir Chave de Licença", - "licenseKeyDeleteConfirm": "Confirmar exclusão da chave de licença", - "licenseTitle": "Gerenciar Status da Licença", - "licenseTitleDescription": "Visualizar e gerenciar chaves de licença no sistema", + "licenseKeyDeleteConfirm": "Confirmar que pretende apagar a chave de licença", + "licenseTitle": "Gerir Status da Licença", + "licenseTitleDescription": "Visualizar e gerir chaves de licença no sistema", "licenseHost": "Licença do host", - "licenseHostDescription": "Gerenciar a chave de licença principal do host.", + "licenseHostDescription": "Gerir a chave de licença principal do host.", "licensedNot": "Não Licenciado", "hostId": "ID do host", "licenseReckeckAll": "Verifique novamente todas as chaves", @@ -370,37 +373,37 @@ "inviteRemoved": "Convite removido", "inviteRemovedDescription": "O convite para {email} foi removido.", "inviteQuestionRemove": "Tem certeza de que deseja remover o convite {email}?", - "inviteMessageRemove": "Uma vez removido, este convite não será mais válido. Você sempre pode convidar o usuário novamente mais tarde.", + "inviteMessageRemove": "Uma vez removido, este convite não será mais válido. Você sempre pode convidar o utilizador novamente mais tarde.", "inviteMessageConfirm": "Para confirmar, digite o endereço de e-mail do convite abaixo.", "inviteQuestionRegenerate": "Tem certeza que deseja regenerar o convite{email, plural, ='' {}, other { para #}}? Isso irá revogar o convite anterior.", "inviteRemoveConfirm": "Confirmar Remoção do Convite", "inviteRegenerated": "Convite Regenerado", "inviteSent": "Um novo convite foi enviado para {email}.", - "inviteSentEmail": "Enviar notificação por e-mail ao usuário", + "inviteSentEmail": "Enviar notificação por e-mail ao utilizador", "inviteGenerate": "Um novo convite foi gerado para {email}.", "inviteDuplicateError": "Convite Duplicado", - "inviteDuplicateErrorDescription": "Já existe um convite para este usuário.", + "inviteDuplicateErrorDescription": "Já existe um convite para este utilizador.", "inviteRateLimitError": "Limite de Taxa Excedido", - "inviteRateLimitErrorDescription": "Você excedeu o limite de 3 regenerações por hora. Por favor, tente novamente mais tarde.", + "inviteRateLimitErrorDescription": "Excedeu o limite de 3 regenerações por hora. Por favor, tente novamente mais tarde.", "inviteRegenerateError": "Falha ao Regenerar Convite", "inviteRegenerateErrorDescription": "Ocorreu um erro ao regenerar o convite.", "inviteValidityPeriod": "Período de Validade", "inviteValidityPeriodSelect": "Selecione o período de validade", - "inviteRegenerateMessage": "O convite foi regenerado. O usuário deve acessar o link abaixo para aceitar o convite.", + "inviteRegenerateMessage": "O convite foi regenerado. O utilizador deve aceder o link abaixo para aceitar o convite.", "inviteRegenerateButton": "Regenerar", "expiresAt": "Expira em", "accessRoleUnknown": "Função Desconhecida", "placeholder": "Espaço reservado", - "userErrorOrgRemove": "Falha ao remover usuário", - "userErrorOrgRemoveDescription": "Ocorreu um erro ao remover o usuário.", + "userErrorOrgRemove": "Falha ao remover utilizador", + "userErrorOrgRemoveDescription": "Ocorreu um erro ao remover o utilizador.", "userOrgRemoved": "Usuário removido", - "userOrgRemovedDescription": "O usuário {email} foi removido da organização.", + "userOrgRemovedDescription": "O utilizador {email} foi removido da organização.", "userQuestionOrgRemove": "Tem certeza que deseja remover {email} da organização?", - "userMessageOrgRemove": "Uma vez removido, este usuário não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.", - "userMessageOrgConfirm": "Para confirmar, digite o nome do usuário abaixo.", + "userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.", + "userMessageOrgConfirm": "Para confirmar, digite o nome do utilizador abaixo.", "userRemoveOrgConfirm": "Confirmar Remoção do Usuário", "userRemoveOrg": "Remover Usuário da Organização", - "users": "Usuários", + "users": "Utilizadores", "accessRoleMember": "Membro", "accessRoleOwner": "Proprietário", "userConfirmed": "Confirmado", @@ -408,7 +411,7 @@ "emailInvalid": "Endereço de email inválido", "inviteValidityDuration": "Por favor, selecione uma duração", "accessRoleSelectPlease": "Por favor, selecione uma função", - "usernameRequired": "Nome de usuário é obrigatório", + "usernameRequired": "Nome de utilizador é obrigatório", "idpSelectPlease": "Por favor, selecione um provedor de identidade", "idpGenericOidc": "Provedor genérico OAuth2/OIDC.", "accessRoleErrorFetch": "Falha ao buscar funções", @@ -416,56 +419,59 @@ "idpErrorFetch": "Falha ao buscar provedores de identidade", "idpErrorFetchDescription": "Ocorreu um erro ao buscar provedores de identidade", "userErrorExists": "Usuário já existe", - "userErrorExistsDescription": "Este usuário já é membro da organização.", - "inviteError": "Falha ao convidar usuário", - "inviteErrorDescription": "Ocorreu um erro ao convidar o usuário", + "userErrorExistsDescription": "Este utilizador já é membro da organização.", + "inviteError": "Falha ao convidar utilizador", + "inviteErrorDescription": "Ocorreu um erro ao convidar o utilizador", "userInvited": "Usuário convidado", - "userInvitedDescription": "O usuário foi convidado com sucesso.", - "userErrorCreate": "Falha ao criar usuário", - "userErrorCreateDescription": "Ocorreu um erro ao criar o usuário", + "userInvitedDescription": "O utilizador foi convidado com sucesso.", + "userErrorCreate": "Falha ao criar utilizador", + "userErrorCreateDescription": "Ocorreu um erro ao criar o utilizador", "userCreated": "Usuário criado", - "userCreatedDescription": "O usuário foi criado com sucesso.", + "userCreatedDescription": "O utilizador foi criado com sucesso.", "userTypeInternal": "Usuário Interno", - "userTypeInternalDescription": "Convidar um usuário para se juntar à sua organização diretamente.", + "userTypeInternalDescription": "Convidar um utilizador para se juntar à sua organização diretamente.", "userTypeExternal": "Usuário Externo", - "userTypeExternalDescription": "Criar um usuário com um provedor de identidade externo.", - "accessUserCreateDescription": "Siga os passos abaixo para criar um novo usuário", - "userSeeAll": "Ver Todos os Usuários", + "userTypeExternalDescription": "Criar um utilizador com um provedor de identidade externo.", + "accessUserCreateDescription": "Siga os passos abaixo para criar um novo utilizador", + "userSeeAll": "Ver Todos os Utilizadores", "userTypeTitle": "Tipo de Usuário", - "userTypeDescription": "Determine como você deseja criar o usuário", + "userTypeDescription": "Determine como você deseja criar o utilizador", "userSettings": "Informações do Usuário", - "userSettingsDescription": "Insira os detalhes para o novo usuário", - "inviteEmailSent": "Enviar e-mail de convite para o usuário", + "userSettingsDescription": "Insira os detalhes para o novo utilizador", + "inviteEmailSent": "Enviar e-mail de convite para o utilizador", "inviteValid": "Válido Por", "selectDuration": "Selecionar duração", "accessRoleSelect": "Selecionar função", - "inviteEmailSentDescription": "Um e-mail foi enviado ao usuário com o link de acesso abaixo. Eles devem acessar o link para aceitar o convite.", - "inviteSentDescription": "O usuário foi convidado. Eles devem acessar o link abaixo para aceitar o convite.", + "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.", "inviteExpiresIn": "O convite expirará em {days, plural, one {# dia} other {# dias}}.", "idpTitle": "Informações Gerais", - "idpSelect": "Selecione o provedor de identidade para o usuário externo", - "idpNotConfigured": "Nenhum provedor de identidade está configurado. Configure um provedor de identidade antes de criar usuários externos.", - "usernameUniq": "Isto deve corresponder ao nome de usuário único que existe no provedor de identidade selecionado.", + "idpSelect": "Selecione o provedor de identidade para o utilizador externo", + "idpNotConfigured": "Nenhum provedor de identidade está configurado. Configure um provedor de identidade antes de criar utilizadores externos.", + "usernameUniq": "Isto deve corresponder ao nome de utilizador único que existe no provedor de identidade selecionado.", "emailOptional": "E-mail (Opcional)", "nameOptional": "Nome (Opcional)", - "accessControls": "Controles de Acesso", - "userDescription2": "Gerenciar as configurações deste usuário", - "accessRoleErrorAdd": "Falha ao adicionar usuário à função", - "accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar usuário à função.", + "accessControls": "Controlos de Acesso", + "userDescription2": "Gerir as configurações deste utilizador", + "accessRoleErrorAdd": "Falha ao adicionar utilizador à função", + "accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar utilizador à função.", "userSaved": "Usuário salvo", - "userSavedDescription": "O usuário foi atualizado.", + "userSavedDescription": "O utilizador foi atualizado.", "autoProvisioned": "Auto provisionado", - "autoProvisionedDescription": "Permitir que este usuário seja gerenciado automaticamente pelo provedor de identidade", - "accessControlsDescription": "Gerencie o que este usuário pode acessar e fazer na organização", - "accessControlsSubmit": "Salvar Controles de Acesso", + "autoProvisionedDescription": "Permitir que este utilizador seja gerido automaticamente pelo provedor de identidade", + "accessControlsDescription": "Gerir o que este utilizador pode aceder e fazer na organização", + "accessControlsSubmit": "Guardar Controlos de Acesso", "roles": "Funções", - "accessUsersRoles": "Gerenciar Usuários e Funções", - "accessUsersRolesDescription": "Convide usuários e adicione-os a funções para gerenciar o acesso à sua organização", + "accessUsersRoles": "Gerir Utilizadores e Funções", + "accessUsersRolesDescription": "Convide utilizadores e adicione-os a funções para gerir o acesso à sua 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 (https)", + "proxyEnableSSL": "Habilitar SSL", + "proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.", + "target": "Target", + "configureTarget": "Configurar Alvos", "targetErrorFetch": "Falha ao buscar alvos", "targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos", "siteErrorFetch": "Falha ao buscar recurso", @@ -492,23 +498,35 @@ "targetTlsSettings": "Configuração de conexão segura", "targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso", "targetTlsSettingsAdvanced": "Configurações TLS Avançadas", - "targetTlsSni": "Nome do Servidor TLS (SNI)", + "targetTlsSni": "Nome do Servidor TLS", "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", - "targetTlsSubmit": "Salvar Configurações", + "targetTlsSubmit": "Guardar Configurações", "targets": "Configuração de Alvos", "targetsDescription": "Configure alvos para rotear tráfego para seus serviços de backend", "targetStickySessions": "Ativar Sessões Persistentes", "targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.", "methodSelect": "Selecionar método", "targetSubmit": "Adicionar Alvo", - "targetNoOne": "Sem alvos. Adicione um alvo usando o formulário.", + "targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.", "targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.", - "targetsSubmit": "Salvar Alvos", + "targetsSubmit": "Guardar Alvos", + "addTarget": "Adicionar Alvo", + "targetErrorInvalidIp": "Endereço IP inválido", + "targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido", + "targetErrorInvalidPort": "Porta inválida", + "targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido", + "targetErrorNoSite": "Nenhum site selecionado", + "targetErrorNoSiteDescription": "Selecione um site para o destino", + "targetCreated": "Destino criado", + "targetCreatedDescription": "O alvo foi criado com sucesso", + "targetErrorCreate": "Falha ao criar destino", + "targetErrorCreateDescription": "Ocorreu um erro ao criar o destino", + "save": "Guardar", "proxyAdditional": "Configurações Adicionais de Proxy", "proxyAdditionalDescription": "Configure como seu recurso lida com 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": "Salvar Configurações de Proxy", + "proxyAdditionalSubmit": "Guardar Configurações de Proxy", "subnetMaskErrorInvalid": "Máscara de subnet inválida. Deve estar entre 0 e 32.", "ipAddressErrorInvalidFormat": "Formato de endereço IP inválido", "ipAddressErrorInvalidOctet": "Octeto de endereço IP inválido", @@ -561,7 +579,7 @@ "ruleSubmit": "Adicionar Regra", "rulesNoOne": "Sem regras. Adicione uma regra usando o formulário.", "rulesOrder": "As regras são avaliadas por prioridade em ordem ascendente.", - "rulesSubmit": "Salvar Regras", + "rulesSubmit": "Guardar Regras", "resourceErrorCreate": "Erro ao criar recurso", "resourceErrorCreateDescription": "Ocorreu um erro ao criar o recurso", "resourceErrorCreateMessage": "Erro ao criar recurso:", @@ -576,7 +594,7 @@ "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.", "resourcesWireGuardConnect": "Conectividade segura com criptografia WireGuard", "resourcesMultipleAuthenticationMethods": "Configure múltiplos métodos de autenticação", - "resourcesUsersRolesAccess": "Controle de acesso baseado em usuários e funções", + "resourcesUsersRolesAccess": "Controle de acesso baseado em utilizadores e funções", "resourcesErrorUpdate": "Falha ao alternar recurso", "resourcesErrorUpdateDescription": "Ocorreu um erro ao atualizar o recurso", "access": "Acesso", @@ -606,7 +624,7 @@ "pangolinSettings": "Configurações - Pangolin", "accessRoleYour": "Sua função:", "accessRoleSelect2": "Selecionar uma função", - "accessUserSelect": "Selecionar um usuário", + "accessUserSelect": "Selecionar um utilizador", "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.", @@ -616,8 +634,8 @@ "otpEmailTitleDescription": "Requer autenticação baseada em e-mail para acesso ao recurso", "otpEmailWhitelist": "Lista de E-mails Permitidos", "otpEmailWhitelistList": "E-mails na Lista Permitida", - "otpEmailWhitelistListDescription": "Apenas usuários com estes endereços de e-mail poderão acessar este recurso. Eles serão solicitados a inserir uma senha única enviada para seu e-mail. Caracteres curinga (*@example.com) podem ser usados para permitir qualquer endereço de e-mail de um domínio.", - "otpEmailWhitelistSave": "Salvar Lista Permitida", + "otpEmailWhitelistListDescription": "Apenas utilizadores com estes endereços de e-mail poderão aceder este recurso. Eles serão solicitados a inserir uma senha única enviada para seu e-mail. Caracteres curinga (*@example.com) podem ser usados para permitir qualquer endereço de e-mail de um domínio.", + "otpEmailWhitelistSave": "Guardar Lista Permitida", "passwordAdd": "Adicionar Senha", "passwordRemove": "Remover Senha", "pincodeAdd": "Adicionar Código PIN", @@ -657,14 +675,14 @@ "resourcePincodeSetupDescription": "O código PIN do recurso foi definido com sucesso", "resourcePincodeSetupTitle": "Definir Código PIN", "resourcePincodeSetupTitleDescription": "Defina um código PIN para proteger este recurso", - "resourceRoleDescription": "Administradores sempre podem acessar este recurso.", - "resourceUsersRoles": "Usuários e Funções", - "resourceUsersRolesDescription": "Configure quais usuários e funções podem visitar este recurso", - "resourceUsersRolesSubmit": "Salvar Usuários e Funções", + "resourceRoleDescription": "Administradores sempre podem aceder este recurso.", + "resourceUsersRoles": "Utilizadores e Funções", + "resourceUsersRolesDescription": "Configure quais utilizadores e funções podem visitar este recurso", + "resourceUsersRolesSubmit": "Guardar Utilizadores e Funções", "resourceWhitelistSave": "Salvo com sucesso", "resourceWhitelistSaveDescription": "As configurações da lista permitida foram salvas", "ssoUse": "Usar SSO da Plataforma", - "ssoUseDescription": "Os usuários existentes só precisarão fazer login uma vez para todos os recursos que tiverem isso habilitado.", + "ssoUseDescription": "Os utilizadores existentes só precisarão fazer login uma vez para todos os recursos que tiverem isso habilitado.", "proxyErrorInvalidPort": "Número da porta inválido", "subdomainErrorInvalid": "Subdomínio inválido", "domainErrorFetch": "Erro ao buscar domínios", @@ -690,7 +708,7 @@ "siteDestination": "Site de Destino", "searchSites": "Pesquisar sites", "accessRoleCreate": "Criar Função", - "accessRoleCreateDescription": "Crie uma nova função para agrupar usuários e gerenciar suas permissões.", + "accessRoleCreateDescription": "Crie uma nova função para agrupar utilizadores e gerir suas permissões.", "accessRoleCreateSubmit": "Criar Função", "accessRoleCreated": "Função criada", "accessRoleCreatedDescription": "A função foi criada com sucesso.", @@ -700,19 +718,19 @@ "accessRoleErrorRemove": "Falha ao remover função", "accessRoleErrorRemoveDescription": "Ocorreu um erro ao remover a função.", "accessRoleName": "Nome da Função", - "accessRoleQuestionRemove": "Você está prestes a excluir a função {name}. Você não pode desfazer esta ação.", + "accessRoleQuestionRemove": "Você está prestes a apagar a função {name}. Você não pode desfazer esta ação.", "accessRoleRemove": "Remover Função", "accessRoleRemoveDescription": "Remover uma função da organização", "accessRoleRemoveSubmit": "Remover Função", "accessRoleRemoved": "Função removida", "accessRoleRemovedDescription": "A função foi removida com sucesso.", - "accessRoleRequiredRemove": "Antes de excluir esta função, selecione uma nova função para transferir os membros existentes.", + "accessRoleRequiredRemove": "Antes de apagar esta função, selecione uma nova função para transferir os membros existentes.", "manage": "Gerir", "sitesNotFound": "Nenhum site encontrado.", "pangolinServerAdmin": "Administrador do Servidor - Pangolin", "licenseTierProfessional": "Licença Profissional", "licenseTierEnterprise": "Licença Empresarial", - "licenseTierCommercial": "Licença comercial", + "licenseTierPersonal": "Personal License", "licensed": "Licenciado", "yes": "Sim", "no": "Não", @@ -747,7 +765,7 @@ "idpDisplayName": "Um nome de exibição para este provedor de identidade", "idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores", "idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.", - "licenseBadge": "Profissional", + "licenseBadge": "EE", "idpType": "Tipo de Provedor", "idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar", "idpOidcConfigure": "Configuração OAuth2/OIDC", @@ -914,12 +932,10 @@ "idpConnectingToFinished": "Conectado", "idpErrorConnectingTo": "Ocorreu um problema ao ligar a {name}. Por favor, contacte o seu administrador.", "idpErrorNotFound": "IdP não encontrado", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Convite Inválido", "inviteInvalidDescription": "O link do convite é inválido.", - "inviteErrorWrongUser": "O convite não é para este usuário", - "inviteErrorUserNotExists": "O usuário não existe. Por favor, crie uma conta primeiro.", + "inviteErrorWrongUser": "O convite não é para este utilizador", + "inviteErrorUserNotExists": "O utilizador não existe. Por favor, crie uma conta primeiro.", "inviteErrorLoginRequired": "Você deve estar logado para aceitar um convite", "inviteErrorExpired": "O convite pode ter expirado", "inviteErrorRevoked": "O convite pode ter sido revogado", @@ -934,7 +950,7 @@ "home": "Início", "accessControl": "Controle de Acesso", "settings": "Configurações", - "usersAll": "Todos os Usuários", + "usersAll": "Todos os Utilizadores", "license": "Licença", "pangolinDashboard": "Painel - Pangolin", "noResults": "Nenhum resultado encontrado.", @@ -987,8 +1003,8 @@ "licenseTierProfessionalRequired": "Edição Profissional Necessária", "licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.", "actionGetOrg": "Obter Organização", - "updateOrgUser": "Atualizar usuário Org", - "createOrgUser": "Criar usuário Org", + "updateOrgUser": "Atualizar utilizador Org", + "createOrgUser": "Criar utilizador Org", "actionUpdateOrg": "Atualizar Organização", "actionUpdateUser": "Atualizar Usuário", "actionGetUser": "Obter Usuário", @@ -1083,7 +1099,6 @@ "navbar": "Menu de Navegação", "navbarDescription": "Menu de navegação principal da aplicação", "navbarDocsLink": "Documentação", - "commercialEdition": "Edição Comercial", "otpErrorEnable": "Não foi possível ativar 2FA", "otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA", "otpSetupCheckCode": "Por favor, insira um código de 6 dígitos", @@ -1135,11 +1150,11 @@ "sidebarRoles": "Papéis", "sidebarShareableLinks": "Links compartilháveis", "sidebarApiKeys": "Chaves API", - "sidebarSettings": "Confirgurações", - "sidebarAllUsers": "Todos os usuários", + "sidebarSettings": "Configurações", + "sidebarAllUsers": "Todos os utilizadores", "sidebarIdentityProviders": "Provedores de identidade", "sidebarLicense": "Tipo:", - "sidebarClients": "Clientes (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Domínios", "enableDockerSocket": "Habilitar o Diagrama Docker", "enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.", @@ -1189,7 +1204,7 @@ "loading": "Carregando", "restart": "Reiniciar", "domains": "Domínios", - "domainsDescription": "Gerencie domínios para sua organização", + "domainsDescription": "Gerir domínios para sua organização", "domainsSearch": "Pesquisar domínios...", "domainAdd": "Adicionar Domínio", "domainAddDescription": "Registre um novo domínio com sua organização", @@ -1217,7 +1232,7 @@ "pending": "Pendente", "sidebarBilling": "Faturamento", "billing": "Faturamento", - "orgBillingDescription": "Gerencie suas informações de faturamento e assinaturas", + "orgBillingDescription": "Gerir suas informações de faturação e assinaturas", "github": "GitHub", "pangolinHosted": "Hospedagem Pangolin", "fossorial": "Fossorial", @@ -1232,7 +1247,7 @@ "completeSetup": "Configuração Completa", "accountSetupSuccess": "Configuração da conta concluída! Bem-vindo ao Pangolin!", "documentation": "Documentação", - "saveAllSettings": "Salvar Todas as Configurações", + "saveAllSettings": "Guardar Todas as Configurações", "settingsUpdated": "Configurações atualizadas", "settingsUpdatedDescription": "Todas as configurações foram atualizadas com sucesso", "settingsErrorUpdate": "Falha ao atualizar configurações", @@ -1257,13 +1272,55 @@ "domainPickerSubdomain": "Subdomínio: {subdomain}", "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mostrar Mais", + "regionSelectorTitle": "Selecionar Região", + "regionSelectorInfo": "Selecionar uma região nos ajuda a fornecer melhor desempenho para sua localização. Você não precisa estar na mesma região que seu servidor.", + "regionSelectorPlaceholder": "Escolher uma região", + "regionSelectorComingSoon": "Em breve", + "billingLoadingSubscription": "Carregando assinatura...", + "billingFreeTier": "Plano Gratuito", + "billingWarningOverLimit": "Aviso: Você ultrapassou um ou mais limites de uso. Seus sites não se conectarão até você modificar sua assinatura ou ajustar seu uso.", + "billingUsageLimitsOverview": "Visão Geral dos Limites de Uso", + "billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@fossorial.io.", + "billingDataUsage": "Uso de Dados", + "billingOnlineTime": "Tempo Online do Site", + "billingUsers": "Usuários Ativos", + "billingDomains": "Domínios Ativos", + "billingRemoteExitNodes": "Nodos Auto-Hospedados Ativos", + "billingNoLimitConfigured": "Nenhum limite configurado", + "billingEstimatedPeriod": "Período Estimado de Cobrança", + "billingIncludedUsage": "Uso Incluído", + "billingIncludedUsageDescription": "Uso incluído no seu plano de assinatura atual", + "billingFreeTierIncludedUsage": "Limites de uso do plano gratuito", + "billingIncluded": "incluído", + "billingEstimatedTotal": "Total Estimado:", + "billingNotes": "Notas", + "billingEstimateNote": "Esta é uma estimativa baseada no seu uso atual.", + "billingActualChargesMayVary": "As cobranças reais podem variar.", + "billingBilledAtEnd": "Sua cobrança será feita ao final do período de cobrança.", + "billingModifySubscription": "Modificar Assinatura", + "billingStartSubscription": "Iniciar Assinatura", + "billingRecurringCharge": "Cobrança Recorrente", + "billingManageSubscriptionSettings": "Gerenciar as configurações e preferências da sua assinatura", + "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", + "billingFailedToGetCheckoutUrl": "Falha ao obter URL de checkout", + "billingPleaseTryAgainLater": "Por favor, tente novamente mais tarde.", + "billingCheckoutError": "Erro de Checkout", + "billingFailedToGetPortalUrl": "Falha ao obter URL do portal", + "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.", "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", "createNewOrgDescription": "Crie uma nova organização", "organization": "Organização", "port": "Porta", - "securityKeyManage": "Gerenciar chaves de segurança", + "securityKeyManage": "Gerir chaves de segurança", "securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha", "securityKeyRegister": "Registrar nova chave de segurança", "securityKeyList": "Suas chaves de segurança", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.", "twoFactor": "Autenticação de Dois Fatores", "adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.", - "continueToApplication": "Continuar para Aplicativo", "securityKeyAdd": "Adicionar Chave de Segurança", "securityKeyRegisterTitle": "Registrar Nova Chave de Segurança", "securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la", @@ -1314,12 +1370,13 @@ "createDomainARecords": "Registros A", "createDomainRecordNumber": "Registrar {number}", "createDomainTxtRecords": "Registros TXT", - "createDomainSaveTheseRecords": "Salvar Esses Registros", + "createDomainSaveTheseRecords": "Guardar Esses Registros", "createDomainSaveTheseRecordsDescription": "Certifique-se de salvar esses registros DNS, pois você não os verá novamente.", "createDomainDnsPropagation": "Propagação DNS", "createDomainDnsPropagationDescription": "Alterações no DNS podem levar algum tempo para se propagar pela internet. Pode levar de alguns minutos a 48 horas, dependendo do seu provedor de DNS e das configurações de TTL.", "resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP", "resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP", + "billingPricingCalculatorLink": "Calculadora de Preços", "signUpTerms": { "IAgreeToThe": "Concordo com", "termsOfService": "os termos de serviço", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Proxy Externo Habilitado", "addNewTarget": "Adicionar Novo Alvo", "targetsList": "Lista de Alvos", + "advancedMode": "Modo Avançado", "targetErrorDuplicateTargetFound": "Alvo duplicado encontrado", + "healthCheckHealthy": "Saudável", + "healthCheckUnhealthy": "Não Saudável", + "healthCheckUnknown": "Desconhecido", + "healthCheck": "Verificação de Saúde", + "configureHealthCheck": "Configurar Verificação de Saúde", + "configureHealthCheckDescription": "Configure a monitorização de saúde para {target}", + "enableHealthChecks": "Ativar Verificações de Saúde", + "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", + "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", + "IntervalSeconds": "Intervalo Saudável", + "timeoutSeconds": "Tempo Limite", + "timeIsInSeconds": "O tempo está em segundos", + "retryAttempts": "Tentativas de Repetição", + "expectedResponseCodes": "Códigos de Resposta Esperados", + "expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.", + "customHeaders": "Cabeçalhos Personalizados", + "customHeadersDescription": "Separados por cabeçalhos da nova linha: Nome do Cabeçalho: valor", + "headersValidationError": "Cabeçalhos devem estar no formato: Nome do Cabeçalho: valor.", + "saveHealthCheck": "Salvar Verificação de Saúde", + "healthCheckSaved": "Verificação de Saúde Salva", + "healthCheckSavedDescription": "Configuração de verificação de saúde salva com sucesso", + "healthCheckError": "Erro de Verificação de Saúde", + "healthCheckErrorDescription": "Ocorreu um erro ao salvar a configuração de verificação de saúde", + "healthCheckPathRequired": "O caminho de verificação de saúde é obrigatório", + "healthCheckMethodRequired": "O método HTTP é obrigatório", + "healthCheckIntervalMin": "O intervalo de verificação deve ser de pelo menos 5 segundos", + "healthCheckTimeoutMin": "O tempo limite deve ser de pelo menos 1 segundo", + "healthCheckRetryMin": "As tentativas de repetição devem ser pelo menos 1", "httpMethod": "Método HTTP", "selectHttpMethod": "Selecionar método HTTP", "domainPickerSubdomainLabel": "Subdomínio", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Digite um subdomínio para buscar e selecionar entre os domínios gratuitos disponíveis.", "domainPickerFreeDomains": "Domínios Gratuitos", "domainPickerSearchForAvailableDomains": "Pesquise por domínios disponíveis", + "domainPickerNotWorkSelfHosted": "Nota: Domínios gratuitos fornecidos não estão disponíveis para instâncias auto-hospedadas no momento.", "resourceDomain": "Domínio", "resourceEditDomain": "Editar Domínio", "siteName": "Nome do Site", @@ -1401,7 +1495,7 @@ "editInternalResourceDialogSitePort": "Porta do Site", "editInternalResourceDialogTargetConfiguration": "Configuração do Alvo", "editInternalResourceDialogCancel": "Cancelar", - "editInternalResourceDialogSaveResource": "Salvar Recurso", + "editInternalResourceDialogSaveResource": "Guardar Recurso", "editInternalResourceDialogSuccess": "Sucesso", "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno atualizado com sucesso", "editInternalResourceDialogError": "Erro", @@ -1428,7 +1522,7 @@ "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", "createInternalResourceDialogSitePort": "Porta do Site", - "createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.", + "createInternalResourceDialogSitePortDescription": "Use esta porta para aceder o recurso no site quando conectado com um cliente.", "createInternalResourceDialogTargetConfiguration": "Configuração do Alvo", "createInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.", "createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.", @@ -1452,7 +1546,7 @@ "siteAddress": "Endereço do Site", "siteAddressDescription": "Especificar o endereço IP do host para que os clientes se conectem. Este é o endereço interno do site na rede Pangolin para os clientes endereçarem. Deve estar dentro da sub-rede da Organização.", "autoLoginExternalIdp": "Login Automático com IDP Externo", - "autoLoginExternalIdpDescription": "Redirecionar imediatamente o usuário para o IDP externo para autenticação.", + "autoLoginExternalIdpDescription": "Redirecionar imediatamente o utilizador para o IDP externo para autenticação.", "selectIdp": "Selecionar IDP", "selectIdpPlaceholder": "Escolher um IDP...", "selectIdpRequired": "Por favor, selecione um IDP quando o login automático estiver ativado.", @@ -1463,6 +1557,72 @@ "autoLoginError": "Erro de Login Automático", "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.", + "remoteExitNodeManageRemoteExitNodes": "Nós remotos", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Nós", + "searchRemoteExitNodes": "Buscar nós...", + "remoteExitNodeAdd": "Adicionar node", + "remoteExitNodeErrorDelete": "Erro ao excluir nó", + "remoteExitNodeQuestionRemove": "Tem certeza que deseja remover o nó {selectedNode} da organização?", + "remoteExitNodeMessageRemove": "Uma vez removido, o nó não estará mais acessível.", + "remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.", + "remoteExitNodeConfirmDelete": "Confirmar exclusão do nó", + "remoteExitNodeDelete": "Excluir nó", + "sidebarRemoteExitNodes": "Nós remotos", + "remoteExitNodeCreate": { + "title": "Criar nó", + "description": "Crie um novo nó para estender sua 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.", + "adopt": { + "title": "Adotar Nodo", + "description": "Escolha isto se você já tem credenciais para o nó." + }, + "generate": { + "title": "Gerar Chaves", + "description": "Escolha esta opção se você quer gerar novas chaves para o nó" + } + }, + "adopt": { + "title": "Adotar Nodo Existente", + "description": "Digite as credenciais do nó existente que deseja adoptar", + "nodeIdLabel": "Nó ID", + "nodeIdDescription": "O ID do nó existente que você deseja adoptar", + "secretLabel": "Chave Secreta", + "secretDescription": "A chave secreta do nó existente", + "submitButton": "Nó Adotado" + }, + "generate": { + "title": "Credenciais Geradas", + "description": "Use estas credenciais geradas para configurar o seu nó", + "nodeIdTitle": "Nó ID", + "secretTitle": "Chave Secreta", + "saveCredentialsTitle": "Adicionar Credenciais à Configuração", + "saveCredentialsDescription": "Adicione essas credenciais ao arquivo de configuração do seu nodo de Pangolin auto-hospedado para completar a conexão.", + "submitButton": "Criar nó" + }, + "validation": { + "adoptRequired": "ID do nó e Segredo são necessários ao adotar um nó existente" + }, + "errors": { + "loadDefaultsFailed": "Falha ao carregar padrões", + "defaultsNotLoaded": "Padrões não carregados", + "createFailed": "Falha ao criar nó" + }, + "success": { + "created": "Nó criado com sucesso" + } + }, + "remoteExitNodeSelection": "Seleção de nó", + "remoteExitNodeSelectionDescription": "Selecione um nó para encaminhar o tráfego para este site local", + "remoteExitNodeRequired": "Um nó deve ser seleccionado para sites locais", + "noRemoteExitNodesAvailable": "Nenhum nó disponível", + "noRemoteExitNodesAvailableDescription": "Nenhum nó está disponível para esta organização. Crie um nó primeiro para usar sites locais.", + "exitNode": "Nodo de Saída", + "country": "País", + "rulesMatchCountry": "Atualmente baseado no IP de origem", "managedSelfHosted": { "title": "Gerenciado Auto-Hospedado", "description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos", @@ -1479,7 +1639,7 @@ }, "benefitLessMaintenance": { "title": "Menos manutenção", - "description": "Sem migrações, backups ou infraestrutura extra para gerenciar. Lidamos com isso na nuvem." + "description": "Sem migrações, backups ou infraestrutura extra para gerir. Lidamos com isso na nuvem." }, "benefitCloudFailover": { "title": "Falha na nuvem", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Domínio Internacional Detectado", "willbestoredas": "Será armazenado como:", + "roleMappingDescription": "Determinar como as funções são atribuídas aos usuários quando eles fazem login quando Auto Provisão está habilitada.", + "selectRole": "Selecione uma função", + "roleMappingExpression": "Expressão", + "selectRolePlaceholder": "Escolha uma função", + "selectRoleDescription": "Selecione uma função para atribuir a todos os usuários deste provedor de identidade", + "roleMappingExpressionDescription": "Insira uma expressão JMESPath para extrair informações da função do token de ID", + "idpTenantIdRequired": "ID do inquilino é necessária", + "invalidValue": "Valor Inválido", + "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", + "idpAzureConfiguration": "Configuração de ID do Azure Entra", + "idpAzureConfigurationDescription": "Configure as suas 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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "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", "idpGoogleDescription": "Provedor Google OAuth2/OIDC", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Cabeçalhos Personalizados", - "headersValidationError": "Cabeçalhos devem estar no formato: Nome do Cabeçalho: valor.", + "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", + "authPageDomain": "Domínio de Página Autenticação", + "noDomainSet": "Nenhum domínio definido", + "changeDomain": "Alterar domínio", + "selectDomain": "Selecionar domínio", + "restartCertificate": "Reiniciar Certificado", + "editAuthPageDomain": "Editar Página de Autenticação", + "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", + "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", "domainPickerVerified": "Verificada", @@ -1518,6 +1721,177 @@ "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", + "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", + "subscriptionRequiredToUse": "Uma assinatura é necessária para usar esse recurso.", + "idpDisabled": "Provedores de identidade estão desabilitados.", + "orgAuthPageDisabled": "A página de autenticação da organização está desativada.", + "domainRestartedDescription": "Verificação de domínio reiniciado com sucesso", "resourceAddEntrypointsEditFile": "Editar arquivo: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Editar arquivo: docker-compose.yml" + "resourceExposePortsEditFile": "Editar arquivo: docker-compose.yml", + "emailVerificationRequired": "Verificação de e-mail é necessária. Por favor, faça login novamente via {dashboardUrl}/auth/login conclui esta etapa. Em seguida, volte aqui.", + "twoFactorSetupRequired": "Configuração de autenticação de dois fatores é necessária. Por favor, entre novamente via {dashboardUrl}/auth/login conclua este passo. Em seguida, volte aqui.", + "authPageErrorUpdateMessage": "Ocorreu um erro ao atualizar as configurações da página de autenticação", + "authPageUpdated": "Página de autenticação atualizada com sucesso", + "healthCheckNotAvailable": "Localização", + "rewritePath": "Reescrever Caminho", + "rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino.", + "continueToApplication": "Continuar para o aplicativo", + "checkingInvite": "Checando convite", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Remover autenticação de cabeçalho", + "resourceHeaderAuthRemoveDescription": "Autenticação de cabeçalho removida com sucesso.", + "resourceErrorHeaderAuthRemove": "Falha ao remover autenticação de cabeçalho", + "resourceErrorHeaderAuthRemoveDescription": "Não foi possível remover a autenticação do cabeçalho para o recurso.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Falha ao definir autenticação de cabeçalho", + "resourceErrorHeaderAuthSetupDescription": "Não foi possível definir a autenticação do cabeçalho para o recurso.", + "resourceHeaderAuthSetup": "Autenticação de Cabeçalho definida com sucesso", + "resourceHeaderAuthSetupDescription": "Autenticação de cabeçalho foi definida com sucesso.", + "resourceHeaderAuthSetupTitle": "Definir autenticação de cabeçalho", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Definir autenticação de cabeçalho", + "actionSetResourceHeaderAuth": "Definir autenticação de cabeçalho", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Prioridade", + "priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 9c38cc11..600725b6 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -96,7 +96,7 @@ "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", "siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", - "siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Просмотреть все сайты", "siteTunnelDescription": "Выберите способ подключения к вашему сайту", "siteNewtCredentials": "Учётные данные Newt", @@ -168,6 +168,9 @@ "siteSelect": "Выберите сайт", "siteSearch": "Поиск сайта", "siteNotFound": "Сайт не найден.", + "selectCountry": "Выберите страну", + "searchCountries": "Поиск стран...", + "noCountryFound": "Страна не найдена.", "siteSelectionDescription": "Этот сайт предоставит подключение к цели.", "resourceType": "Тип ресурса", "resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу", @@ -236,7 +239,7 @@ "accessUserCreate": "Создать пользователя", "accessUserRemove": "Удалить пользователя", "username": "Имя пользователя", - "identityProvider": "Identity Provider", + "identityProvider": "Поставщик удостоверений", "role": "Роль", "nameRequired": "Имя обязательно", "accessRolesManage": "Управление ролями", @@ -465,7 +468,10 @@ "createdAt": "Создано в", "proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.", "proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.", - "proxyEnableSSL": "Включить SSL (https)", + "proxyEnableSSL": "Включить SSL", + "proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.", + "target": "Target", + "configureTarget": "Настроить адресаты", "targetErrorFetch": "Не удалось получить цели", "targetErrorFetchDescription": "Произошла ошибка при получении целей", "siteErrorFetch": "Не удалось получить ресурс", @@ -492,7 +498,7 @@ "targetTlsSettings": "Конфигурация безопасного соединения", "targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса", "targetTlsSettingsAdvanced": "Расширенные настройки TLS", - "targetTlsSni": "Имя TLS сервера (SNI)", + "targetTlsSni": "Имя TLS сервера", "targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.", "targetTlsSubmit": "Сохранить настройки", "targets": "Конфигурация целей", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.", "methodSelect": "Выберите метод", "targetSubmit": "Добавить цель", - "targetNoOne": "Нет целей. Добавьте цель с помощью формы.", + "targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы к вашему бэкэнду.", "targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.", "targetsSubmit": "Сохранить цели", + "addTarget": "Добавить цель", + "targetErrorInvalidIp": "Неверный IP-адрес", + "targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста", + "targetErrorInvalidPort": "Неверный порт", + "targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта", + "targetErrorNoSite": "Сайт не выбран", + "targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели", + "targetCreated": "Цель создана", + "targetCreatedDescription": "Цель была успешно создана", + "targetErrorCreate": "Не удалось создать цель", + "targetErrorCreateDescription": "Произошла ошибка при создании цели", + "save": "Сохранить", "proxyAdditional": "Дополнительные настройки прокси", "proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси", "proxyCustomHeader": "Пользовательский заголовок Host", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Администратор сервера - Pangolin", "licenseTierProfessional": "Профессиональная лицензия", "licenseTierEnterprise": "Корпоративная лицензия", - "licenseTierCommercial": "Коммерческая лицензия", + "licenseTierPersonal": "Personal License", "licensed": "Лицензировано", "yes": "Да", "no": "Нет", @@ -747,7 +765,7 @@ "idpDisplayName": "Отображаемое имя для этого поставщика удостоверений", "idpAutoProvisionUsers": "Автоматическое создание пользователей", "idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.", - "licenseBadge": "Профессиональная", + "licenseBadge": "EE", "idpType": "Тип поставщика", "idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить", "idpOidcConfigure": "Конфигурация OAuth2/OIDC", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Подключено", "idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.", "idpErrorNotFound": "IdP не найден", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Недействительное приглашение", "inviteInvalidDescription": "Ссылка на приглашение недействительна.", "inviteErrorWrongUser": "Приглашение не для этого пользователя", @@ -1083,7 +1099,6 @@ "navbar": "Навигационное меню", "navbarDescription": "Главное навигационное меню приложения", "navbarDocsLink": "Документация", - "commercialEdition": "Коммерческая версия", "otpErrorEnable": "Невозможно включить 2FA", "otpErrorEnableDescription": "Произошла ошибка при включении 2FA", "otpSetupCheckCode": "Пожалуйста, введите 6-значный код", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Все пользователи", "sidebarIdentityProviders": "Поставщики удостоверений", "sidebarLicense": "Лицензия", - "sidebarClients": "Клиенты (бета)", + "sidebarClients": "Clients", "sidebarDomains": "Домены", "enableDockerSocket": "Включить чертёж Docker", "enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Поддомен: {subdomain}", "domainPickerNamespace": "Пространство имен: {namespace}", "domainPickerShowMore": "Показать еще", + "regionSelectorTitle": "Выберите регион", + "regionSelectorInfo": "Выбор региона помогает нам обеспечить лучшее качество обслуживания для вашего расположения. Вам необязательно находиться в том же регионе, что и ваш сервер.", + "regionSelectorPlaceholder": "Выбор региона", + "regionSelectorComingSoon": "Скоро будет", + "billingLoadingSubscription": "Загрузка подписки...", + "billingFreeTier": "Бесплатный уровень", + "billingWarningOverLimit": "Предупреждение: Вы превысили одну или несколько границ использования. Ваши сайты не подключатся, пока вы не измените подписку или не скорректируете использование.", + "billingUsageLimitsOverview": "Обзор лимитов использования", + "billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@fossorial.io.", + "billingDataUsage": "Использование данных", + "billingOnlineTime": "Время работы сайта", + "billingUsers": "Активные пользователи", + "billingDomains": "Активные домены", + "billingRemoteExitNodes": "Активные самоуправляемые узлы", + "billingNoLimitConfigured": "Лимит не установлен", + "billingEstimatedPeriod": "Предполагаемый период выставления счетов", + "billingIncludedUsage": "Включенное использование", + "billingIncludedUsageDescription": "Использование, включенное в ваш текущий план подписки", + "billingFreeTierIncludedUsage": "Бесплатное использование ограничений", + "billingIncluded": "включено", + "billingEstimatedTotal": "Предполагаемая сумма:", + "billingNotes": "Заметки", + "billingEstimateNote": "Это приблизительная оценка на основании вашего текущего использования.", + "billingActualChargesMayVary": "Фактические начисления могут отличаться.", + "billingBilledAtEnd": "С вас будет выставлен счет в конце периода выставления счетов.", + "billingModifySubscription": "Изменить подписку", + "billingStartSubscription": "Начать подписку", + "billingRecurringCharge": "Периодический взнос", + "billingManageSubscriptionSettings": "Управляйте настройками и предпочтениями вашей подписки", + "billingNoActiveSubscription": "У вас нет активной подписки. Начните подписку, чтобы увеличить лимиты использования.", + "billingFailedToLoadSubscription": "Не удалось загрузить подписку", + "billingFailedToLoadUsage": "Не удалось загрузить использование", + "billingFailedToGetCheckoutUrl": "Не удалось получить URL-адрес для оплаты", + "billingPleaseTryAgainLater": "Пожалуйста, повторите попытку позже.", + "billingCheckoutError": "Ошибка при оформлении заказа", + "billingFailedToGetPortalUrl": "Не удалось получить URL-адрес портала", + "billingPortalError": "Ошибка портала", + "billingDataUsageInfo": "Вы несете ответственность за все данные, переданные через безопасные туннели при подключении к облаку. Это включает как входящий, так и исходящий трафик на всех ваших сайтах. При достижении лимита ваши сайты будут отключаться до тех пор, пока вы не обновите план или не уменьшите его использование. При использовании узлов не взимается плата.", + "billingOnlineTimeInfo": "Вы тарифицируете на то, как долго ваши сайты будут подключены к облаку. Например, 44 640 минут равны одному сайту, работающему круглосуточно за весь месяц. Когда вы достигните лимита, ваши сайты будут отключаться до тех пор, пока вы не обновите тарифный план или не сократите нагрузку. При использовании узлов не тарифицируется.", + "billingUsersInfo": "С вас взимается плата за каждого пользователя в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных учетных записей пользователей в вашей организации.", + "billingDomainInfo": "С вас взимается плата за каждый домен в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных учетных записей доменов в вашей организации.", + "billingRemoteExitNodesInfo": "С вас взимается плата за каждый управляемый узел в вашей организации. Оплата рассчитывается ежедневно исходя из количества активных управляемых узлов в вашей организации.", "domainNotFound": "Домен не найден", "domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.", "failed": "Ошибка", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.", "twoFactor": "Двухфакторная аутентификация", "adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.", - "continueToApplication": "Перейти к приложению", "securityKeyAdd": "Добавить ключ безопасности", "securityKeyRegisterTitle": "Регистрация нового ключа безопасности", "securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "Изменения DNS могут занять некоторое время для распространения через интернет. Это может занять от нескольких минут до 48 часов в зависимости от вашего DNS провайдера и настроек TTL.", "resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов", "resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов", + "billingPricingCalculatorLink": "Калькулятор расценок", "signUpTerms": { "IAgreeToThe": "Я согласен с", "termsOfService": "условия использования", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Внешний прокси включен", "addNewTarget": "Добавить новую цель", "targetsList": "Список целей", + "advancedMode": "Расширенный режим", "targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель", + "healthCheckHealthy": "Здоровый", + "healthCheckUnhealthy": "Нездоровый", + "healthCheckUnknown": "Неизвестно", + "healthCheck": "Проверка здоровья", + "configureHealthCheck": "Настроить проверку здоровья", + "configureHealthCheckDescription": "Настройте мониторинг состояния для {target}", + "enableHealthChecks": "Включить проверки здоровья", + "enableHealthChecksDescription": "Мониторинг здоровья этой цели. При необходимости можно контролировать другую конечную точку.", + "healthScheme": "Метод", + "healthSelectScheme": "Выберите метод", + "healthCheckPath": "Путь", + "healthHostname": "IP / хост", + "healthPort": "Порт", + "healthCheckPathDescription": "Путь к проверке состояния здоровья.", + "healthyIntervalSeconds": "Интервал здоровых состояний", + "unhealthyIntervalSeconds": "Интервал нездоровых состояний", + "IntervalSeconds": "Интервал здоровых состояний", + "timeoutSeconds": "Тайм-аут", + "timeIsInSeconds": "Время указано в секундах", + "retryAttempts": "Количество попыток повторного запроса", + "expectedResponseCodes": "Ожидаемые коды ответов", + "expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.", + "customHeaders": "Пользовательские заголовки", + "customHeadersDescription": "Заголовки новой строки, разделённые: название заголовка: значение", + "headersValidationError": "Заголовки должны быть в формате: Название заголовка: значение.", + "saveHealthCheck": "Сохранить проверку здоровья", + "healthCheckSaved": "Проверка здоровья сохранена", + "healthCheckSavedDescription": "Конфигурация проверки состояния успешно сохранена", + "healthCheckError": "Ошибка проверки состояния", + "healthCheckErrorDescription": "Произошла ошибка при сохранении конфигурации проверки состояния", + "healthCheckPathRequired": "Требуется путь проверки состояния", + "healthCheckMethodRequired": "Требуется метод HTTP", + "healthCheckIntervalMin": "Интервал проверки должен составлять не менее 5 секунд", + "healthCheckTimeoutMin": "Тайм-аут должен составлять не менее 1 секунды", + "healthCheckRetryMin": "Количество попыток должно быть не менее 1", "httpMethod": "HTTP метод", "selectHttpMethod": "Выберите HTTP метод", "domainPickerSubdomainLabel": "Поддомен", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Введите поддомен для поиска и выбора из доступных свободных доменов.", "domainPickerFreeDomains": "Свободные домены", "domainPickerSearchForAvailableDomains": "Поиск доступных доменов", + "domainPickerNotWorkSelfHosted": "Примечание: бесплатные предоставляемые домены в данный момент недоступны для самоуправляемых экземпляров.", "resourceDomain": "Домен", "resourceEditDomain": "Редактировать домен", "siteName": "Имя сайта", @@ -1463,6 +1557,72 @@ "autoLoginError": "Ошибка автоматического входа", "autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.", "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.", + "remoteExitNodeManageRemoteExitNodes": "Удаленные узлы", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Узлы", + "searchRemoteExitNodes": "Поиск узлов...", + "remoteExitNodeAdd": "Добавить узел", + "remoteExitNodeErrorDelete": "Ошибка удаления узла", + "remoteExitNodeQuestionRemove": "Вы уверены, что хотите удалить узел {selectedNode} из организации?", + "remoteExitNodeMessageRemove": "После удаления узел больше не будет доступен.", + "remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.", + "remoteExitNodeConfirmDelete": "Подтвердите удаление узла", + "remoteExitNodeDelete": "Удалить узел", + "sidebarRemoteExitNodes": "Удаленные узлы", + "remoteExitNodeCreate": { + "title": "Создать узел", + "description": "Создайте новый узел, чтобы расширить сетевое подключение", + "viewAllButton": "Все узлы", + "strategy": { + "title": "Стратегия создания", + "description": "Выберите эту опцию для настройки вашего узла или создания новых учетных данных.", + "adopt": { + "title": "Принять узел", + "description": "Выберите это, если у вас уже есть учетные данные для узла." + }, + "generate": { + "title": "Сгенерировать ключи", + "description": "Выберите это, если вы хотите создать новые ключи для узла" + } + }, + "adopt": { + "title": "Принять существующий узел", + "description": "Введите учетные данные существующего узла, который вы хотите принять", + "nodeIdLabel": "ID узла", + "nodeIdDescription": "ID существующего узла, который вы хотите принять", + "secretLabel": "Секретный ключ", + "secretDescription": "Секретный ключ существующего узла", + "submitButton": "Принять узел" + }, + "generate": { + "title": "Сгенерированные учетные данные", + "description": "Используйте эти учётные данные для настройки вашего узла", + "nodeIdTitle": "ID узла", + "secretTitle": "Секретный ключ", + "saveCredentialsTitle": "Добавить учетные данные в конфигурацию", + "saveCredentialsDescription": "Добавьте эти учетные данные в файл конфигурации вашего самоуправляемого узла Pangolin, чтобы завершить подключение.", + "submitButton": "Создать узел" + }, + "validation": { + "adoptRequired": "ID узла и секрет требуются при установке существующего узла" + }, + "errors": { + "loadDefaultsFailed": "Не удалось загрузить параметры по умолчанию", + "defaultsNotLoaded": "Параметры по умолчанию не загружены", + "createFailed": "Не удалось создать узел" + }, + "success": { + "created": "Узел успешно создан" + } + }, + "remoteExitNodeSelection": "Выбор узла", + "remoteExitNodeSelectionDescription": "Выберите узел для маршрутизации трафика для этого локального сайта", + "remoteExitNodeRequired": "Узел должен быть выбран для локальных сайтов", + "noRemoteExitNodesAvailable": "Нет доступных узлов", + "noRemoteExitNodesAvailableDescription": "Для этой организации узлы не доступны. Сначала создайте узел, чтобы использовать локальные сайты.", + "exitNode": "Узел выхода", + "country": "Страна", + "rulesMatchCountry": "В настоящее время основано на исходном IP", "managedSelfHosted": { "title": "Управляемый с самовывоза", "description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Обнаружен международный домен", "willbestoredas": "Будет храниться как:", + "roleMappingDescription": "Определите, как роли, назначаемые пользователям, когда они войдут в систему автоматического профиля.", + "selectRole": "Выберите роль", + "roleMappingExpression": "Выражение", + "selectRolePlaceholder": "Выберите роль", + "selectRoleDescription": "Выберите роль, чтобы назначить всем пользователям этого поставщика идентификации", + "roleMappingExpressionDescription": "Введите выражение JMESPath, чтобы извлечь информацию о роли из ID токена", + "idpTenantIdRequired": "Требуется ID владельца", + "invalidValue": "Неверное значение", + "idpTypeLabel": "Тип поставщика удостоверений", + "roleMappingExpressionPlaceholder": "например, contains(groups, 'admin') && 'Admin' || 'Member'", + "idpGoogleConfiguration": "Конфигурация Google", + "idpGoogleConfigurationDescription": "Настройка учетных данных Google OAuth2", + "idpGoogleClientIdDescription": "Ваш Google OAuth2 ID клиента", + "idpGoogleClientSecretDescription": "Ваш Google OAuth2 Секрет", + "idpAzureConfiguration": "Конфигурация Azure Entra ID", + "idpAzureConfigurationDescription": "Настройте учетные данные Azure Entra ID OAuth2", + "idpTenantId": "Идентификатор арендатора", + "idpTenantIdPlaceholder": "ваш тенант-id", + "idpAzureTenantIdDescription": "Идентификатор арендатора Azure (найден в обзоре Active Directory Azure)", + "idpAzureClientIdDescription": "Ваш идентификатор клиента Azure App", + "idpAzureClientSecretDescription": "Секрет регистрации клиента Azure App", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Конфигурация Google", + "idpAzureConfigurationTitle": "Конфигурация Azure Entra ID", + "idpTenantIdLabel": "Идентификатор арендатора", + "idpAzureClientIdDescription2": "Ваш идентификатор клиента Azure App", + "idpAzureClientSecretDescription2": "Секрет регистрации клиента Azure App", "idpGoogleDescription": "Google OAuth2/OIDC провайдер", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "Пользовательские заголовки", - "headersValidationError": "Заголовки должны быть в формате: Название заголовка: значение.", + "subnet": "Подсеть", + "subnetDescription": "Подсеть для конфигурации сети этой организации.", + "authPage": "Страница авторизации", + "authPageDescription": "Настройка страницы авторизации для вашей организации", + "authPageDomain": "Домен страницы авторизации", + "noDomainSet": "Домен не установлен", + "changeDomain": "Изменить домен", + "selectDomain": "Выберите домен", + "restartCertificate": "Перезапустить сертификат", + "editAuthPageDomain": "Редактировать домен страницы авторизации", + "setAuthPageDomain": "Установить домен страницы авторизации", + "failedToFetchCertificate": "Не удалось получить сертификат", + "failedToRestartCertificate": "Не удалось перезапустить сертификат", + "addDomainToEnableCustomAuthPages": "Добавьте домен для включения пользовательских страниц аутентификации для вашей организации", + "selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации", "domainPickerProvidedDomain": "Домен предоставлен", "domainPickerFreeProvidedDomain": "Бесплатный домен", "domainPickerVerified": "Подтверждено", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не может быть действительным для {domain}.", "domainPickerSubdomainSanitized": "Субдомен очищен", "domainPickerSubdomainCorrected": "\"{sub}\" был исправлен на \"{sanitized}\"", + "orgAuthSignInTitle": "Войдите в свою организацию", + "orgAuthChooseIdpDescription": "Выберите своего поставщика удостоверений личности для продолжения", + "orgAuthNoIdpConfigured": "Эта организация не имеет настроенных поставщиков идентификационных данных. Вместо этого вы можете войти в свой Pangolin.", + "orgAuthSignInWithPangolin": "Войти через Pangolin", + "subscriptionRequiredToUse": "Для использования этой функции требуется подписка.", + "idpDisabled": "Провайдеры идентификации отключены.", + "orgAuthPageDisabled": "Страница авторизации организации отключена.", + "domainRestartedDescription": "Проверка домена успешно перезапущена", "resourceAddEntrypointsEditFile": "Редактировать файл: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Редактировать файл: docker-compose.yml" + "resourceExposePortsEditFile": "Редактировать файл: docker-compose.yml", + "emailVerificationRequired": "Требуется подтверждение адреса электронной почты. Пожалуйста, войдите снова через {dashboardUrl}/auth/login завершить этот шаг. Затем вернитесь сюда.", + "twoFactorSetupRequired": "Требуется настройка двухфакторной аутентификации. Пожалуйста, войдите снова через {dashboardUrl}/auth/login завершить этот шаг. Затем вернитесь сюда.", + "authPageErrorUpdateMessage": "Произошла ошибка при обновлении настроек страницы авторизации", + "authPageUpdated": "Страница авторизации успешно обновлена", + "healthCheckNotAvailable": "Локальный", + "rewritePath": "Переписать путь", + "rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу.", + "continueToApplication": "Перейти к приложению", + "checkingInvite": "Проверка приглашения", + "setResourceHeaderAuth": "установить заголовок ресурса", + "resourceHeaderAuthRemove": "Удалить проверку подлинности заголовка", + "resourceHeaderAuthRemoveDescription": "Проверка подлинности заголовка успешно удалена.", + "resourceErrorHeaderAuthRemove": "Не удалось удалить аутентификацию заголовка", + "resourceErrorHeaderAuthRemoveDescription": "Не удалось удалить проверку подлинности заголовка ресурса.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Не удалось установить аутентификацию заголовка", + "resourceErrorHeaderAuthSetupDescription": "Не удалось установить проверку подлинности заголовка ресурса.", + "resourceHeaderAuthSetup": "Проверка подлинности заголовка успешно установлена", + "resourceHeaderAuthSetupDescription": "Проверка подлинности заголовка успешно установлена.", + "resourceHeaderAuthSetupTitle": "Установить проверку подлинности заголовка", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Установить проверку подлинности заголовка", + "actionSetResourceHeaderAuth": "Установить проверку подлинности заголовка", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Приоритет", + "priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index ef812850..b5b99888 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -96,7 +96,7 @@ "siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.", "siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", "siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.", - "siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "Tüm Siteleri Gör", "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", "siteNewtCredentials": "Newt Kimlik Bilgileri", @@ -168,6 +168,9 @@ "siteSelect": "Site seç", "siteSearch": "Site ara", "siteNotFound": "Herhangi bir site bulunamadı.", + "selectCountry": "Ülke Seç", + "searchCountries": "Ülkeleri ara...", + "noCountryFound": "Ülke bulunamadı.", "siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.", "resourceType": "Kaynak Türü", "resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin", @@ -465,7 +468,10 @@ "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'yi Etkinleştir (https)", + "proxyEnableSSL": "SSL Etkinleştir", + "proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.", + "target": "Hedef", + "configureTarget": "Hedefleri Yapılandır", "targetErrorFetch": "Hedefleri alamadı", "targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu", "siteErrorFetch": "kaynağa ulaşılamadı", @@ -492,7 +498,7 @@ "targetTlsSettings": "HTTPS & TLS Settings", "targetTlsSettingsDescription": "Configure TLS settings for your resource", "targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları", - "targetTlsSni": "TLS Sunucu Adı (SNI)", + "targetTlsSni": "TLS Sunucu Adı", "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", "targetTlsSubmit": "Ayarları Kaydet", "targets": "Hedefler Konfigürasyonu", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.", "methodSelect": "Yöntemi Seç", "targetSubmit": "Hedef Ekle", - "targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir hedef ekleyin.", + "targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.", "targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.", "targetsSubmit": "Hedefleri Kaydet", + "addTarget": "Hedef Ekle", + "targetErrorInvalidIp": "Geçersiz IP adresi", + "targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin", + "targetErrorInvalidPort": "Geçersiz port", + "targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin", + "targetErrorNoSite": "Hiçbir site seçili değil", + "targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin", + "targetCreated": "Hedef oluşturuldu", + "targetCreatedDescription": "Hedef başarıyla oluşturuldu", + "targetErrorCreate": "Hedef oluşturma başarısız oldu", + "targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu", + "save": "Kaydet", "proxyAdditional": "Ek Proxy Ayarları", "proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın", "proxyCustomHeader": "Özel Ana Bilgisayar Başlığı", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin", "licenseTierProfessional": "Profesyonel Lisans", "licenseTierEnterprise": "Kurumsal Lisans", - "licenseTierCommercial": "Ticari Lisans", + "licenseTierPersonal": "Personal License", "licensed": "Lisanslı", "yes": "Evet", "no": "Hayır", @@ -747,7 +765,7 @@ "idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı", "idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla", "idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.", - "licenseBadge": "Profesyonel", + "licenseBadge": "EE", "idpType": "Sağlayıcı Türü", "idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin", "idpOidcConfigure": "OAuth2/OIDC Yapılandırması", @@ -814,7 +832,7 @@ "redirectUrl": "Yönlendirme URL'si", "redirectUrlAbout": "Yönlendirme URL'si Hakkında", "redirectUrlAboutDescription": "Bu, kimlik doğrulamasından sonra kullanıcıların yönlendirileceği URL'dir. Bu URL'yi kimlik sağlayıcınızın ayarlarında yapılandırmanız gerekir.", - "pangolinAuth": "Auth - Pangolin", + "pangolinAuth": "Yetkilendirme - Pangolin", "verificationCodeLengthRequirements": "Doğrulama kodunuz 8 karakter olmalıdır.", "errorOccurred": "Bir hata oluştu", "emailErrorVerify": "E-posta doğrulanamadı: ", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "Bağlandı", "idpErrorConnectingTo": "{name} ile bağlantı kurarken bir sorun meydana geldi. Lütfen yöneticiye danışın.", "idpErrorNotFound": "IdP bulunamadı", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "Geçersiz Davet", "inviteInvalidDescription": "Davet bağlantısı geçersiz.", "inviteErrorWrongUser": "Davet bu kullanıcı için değil", @@ -1083,7 +1099,6 @@ "navbar": "Navigasyon Menüsü", "navbarDescription": "Uygulamanın ana navigasyon menüsü", "navbarDocsLink": "Dokümantasyon", - "commercialEdition": "Ticari Sürüm", "otpErrorEnable": "2FA etkinleştirilemedi", "otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu", "otpSetupCheckCode": "6 haneli bir kod girin", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "Tüm Kullanıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarLicense": "Lisans", - "sidebarClients": "Müşteriler (Beta)", + "sidebarClients": "Clients", "sidebarDomains": "Alan Adları", "enableDockerSocket": "Docker Soketini Etkinleştir", "enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.", @@ -1241,7 +1256,7 @@ "sidebarExpand": "Genişlet", "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": "Domain", + "domainPickerEnterDomain": "Alan Adı", "domainPickerPlaceholder": "myapp.example.com", "domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.", "domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "Alt Alan: {subdomain}", "domainPickerNamespace": "Ad Alanı: {namespace}", "domainPickerShowMore": "Daha Fazla Göster", + "regionSelectorTitle": "Bölge Seç", + "regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.", + "regionSelectorPlaceholder": "Bölge Seçin", + "regionSelectorComingSoon": "Yakında Geliyor", + "billingLoadingSubscription": "Abonelik yükleniyor...", + "billingFreeTier": "Ücretsiz Dilim", + "billingWarningOverLimit": "Uyarı: Bir veya daha fazla kullanım limitini aştınız. Aboneliğinizi değiştirmediğiniz veya kullanımı ayarlamadığınız sürece siteleriniz bağlanmayacaktır.", + "billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü", + "billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@fossorial.io adresinden bizimle iletişime geçin.", + "billingDataUsage": "Veri Kullanımı", + "billingOnlineTime": "Site Çevrimiçi Süresi", + "billingUsers": "Aktif Kullanıcılar", + "billingDomains": "Aktif Alanlar", + "billingRemoteExitNodes": "Aktif Öz-Host Düğümleri", + "billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı", + "billingEstimatedPeriod": "Tahmini Fatura Dönemi", + "billingIncludedUsage": "Dahil Kullanım", + "billingIncludedUsageDescription": "Mevcut abonelik planınıza bağlı kullanım", + "billingFreeTierIncludedUsage": "Ücretsiz dilim kullanım hakları", + "billingIncluded": "dahil", + "billingEstimatedTotal": "Tahmini Toplam:", + "billingNotes": "Notlar", + "billingEstimateNote": "Bu, mevcut kullanımınıza dayalı bir tahmindir.", + "billingActualChargesMayVary": "Asıl ücretler farklılık gösterebilir.", + "billingBilledAtEnd": "Fatura döneminin sonunda fatura düzenlenecektir.", + "billingModifySubscription": "Aboneliği Düzenle", + "billingStartSubscription": "Aboneliği Başlat", + "billingRecurringCharge": "Yinelenen Ücret", + "billingManageSubscriptionSettings": "Abonelik ayarlarınızı ve tercihlerinizi yönetin", + "billingNoActiveSubscription": "Aktif bir aboneliğiniz yok. Kullanım limitlerini artırmak için aboneliğinizi başlatın.", + "billingFailedToLoadSubscription": "Abonelik yüklenemedi", + "billingFailedToLoadUsage": "Kullanım yüklenemedi", + "billingFailedToGetCheckoutUrl": "Ödeme URL'si alınamadı", + "billingPleaseTryAgainLater": "Lütfen daha sonra tekrar deneyin.", + "billingCheckoutError": "Ödeme Hatası", + "billingFailedToGetPortalUrl": "Portal URL'si alınamadı", + "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.", "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", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.", "twoFactor": "İki Faktörlü Kimlik Doğrulama", "adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.", - "continueToApplication": "Uygulamaya Devam Et", "securityKeyAdd": "Güvenlik Anahtarı Ekle", "securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet", "securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS değişikliklerinin internet genelinde yayılması zaman alabilir. DNS sağlayıcınız ve TTL ayarlarına bağlı olarak bu birkaç dakika ile 48 saat arasında değişebilir.", "resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir", "resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı", + "billingPricingCalculatorLink": "Fiyat Hesaplayıcı", "signUpTerms": { "IAgreeToThe": "Kabul ediyorum", "termsOfService": "hizmet şartları", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "Dış Proxy Etkinleştirildi", "addNewTarget": "Yeni Hedef Ekle", "targetsList": "Hedefler Listesi", + "advancedMode": "Gelişmiş Mod", "targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu", + "healthCheckHealthy": "Sağlıklı", + "healthCheckUnhealthy": "Sağlıksız", + "healthCheckUnknown": "Bilinmiyor", + "healthCheck": "Sağlık Kontrolü", + "configureHealthCheck": "Sağlık Kontrolünü Yapılandır", + "configureHealthCheckDescription": "{hedef} için sağlık izleme kurun", + "enableHealthChecks": "Sağlık Kontrollerini Etkinleştir", + "enableHealthChecksDescription": "Bu hedefin sağlığını izleyin. Gerekirse hedef dışındaki bir son noktayı izleyebilirsiniz.", + "healthScheme": "Yöntem", + "healthSelectScheme": "Yöntem Seç", + "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", + "IntervalSeconds": "Sağlıklı Aralık", + "timeoutSeconds": "Zaman Aşımı", + "timeIsInSeconds": "Zaman saniye cinsindendir", + "retryAttempts": "Tekrar Deneme Girişimleri", + "expectedResponseCodes": "Beklenen Yanıt Kodları", + "expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.", + "customHeaders": "Özel Başlıklar", + "customHeadersDescription": "Başlıklar yeni satırla ayrılmış: Başlık-Adı: değer", + "headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.", + "saveHealthCheck": "Sağlık Kontrolünü Kaydet", + "healthCheckSaved": "Sağlık Kontrolü Kaydedildi", + "healthCheckSavedDescription": "Sağlık kontrol yapılandırması başarıyla kaydedildi", + "healthCheckError": "Sağlık Kontrol Hatası", + "healthCheckErrorDescription": "Sağlık kontrol yapılandırması kaydedilirken bir hata oluştu", + "healthCheckPathRequired": "Sağlık kontrol yolu gereklidir", + "healthCheckMethodRequired": "HTTP yöntemi gereklidir", + "healthCheckIntervalMin": "Kontrol aralığı en az 5 saniye olmalıdır", + "healthCheckTimeoutMin": "Zaman aşımı en az 1 saniye olmalıdır", + "healthCheckRetryMin": "Tekrar deneme girişimleri en az 1 olmalıdır", "httpMethod": "HTTP Yöntemi", "selectHttpMethod": "HTTP yöntemini seçin", "domainPickerSubdomainLabel": "Alt Alan Adı", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "Mevcut ücretsiz alan adları arasından aramak ve seçmek için bir alt alan adı girin.", "domainPickerFreeDomains": "Ücretsiz Alan Adları", "domainPickerSearchForAvailableDomains": "Mevcut alan adlarını ara", + "domainPickerNotWorkSelfHosted": "Not: Ücretsiz sağlanan alan adları şu anda öz-host edilmiş örnekler için kullanılabilir değildir.", "resourceDomain": "Alan Adı", "resourceEditDomain": "Alan Adını Düzenle", "siteName": "Site Adı", @@ -1463,6 +1557,72 @@ "autoLoginError": "Otomatik Giriş Hatası", "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": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "Düğümler", + "searchRemoteExitNodes": "Düğüm ara...", + "remoteExitNodeAdd": "Düğüm Ekle", + "remoteExitNodeErrorDelete": "Düğüm silinirken hata oluştu", + "remoteExitNodeQuestionRemove": "{selectedNode} düğümünü organizasyondan kaldırmak istediğinizden emin misiniz?", + "remoteExitNodeMessageRemove": "Kaldırıldığında, düğüm artık erişilebilir olmayacaktır.", + "remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.", + "remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla", + "remoteExitNodeDelete": "Düğümü Sil", + "sidebarRemoteExitNodes": "Uzak Düğümler", + "remoteExitNodeCreate": { + "title": "Düğüm Oluştur", + "description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun", + "viewAllButton": "Tüm Düğümleri Gör", + "strategy": { + "title": "Oluşturma Stratejisi", + "description": "Düğümünüzü manuel olarak yapılandırmak veya yeni kimlik bilgileri oluşturmak için bunu seçin.", + "adopt": { + "title": "Düğüm Benimse", + "description": "Zaten düğüm için kimlik bilgilerine sahipseniz bunu seçin." + }, + "generate": { + "title": "Anahtarları Oluştur", + "description": "Düğüm için yeni anahtarlar oluşturmak istiyorsanız bunu seçin" + } + }, + "adopt": { + "title": "Mevcut Düğümü Benimse", + "description": "Adayacağınız mevcut düğümün kimlik bilgilerini girin", + "nodeIdLabel": "Düğüm ID", + "nodeIdDescription": "Adayacağınız mevcut düğümün ID'si", + "secretLabel": "Gizli", + "secretDescription": "Mevcut düğümün gizli anahtarı", + "submitButton": "Düğümü Benimse" + }, + "generate": { + "title": "Oluşturulan Kimlik Bilgileri", + "description": "Düğümünüzü yapılandırmak için oluşturulan bu kimlik bilgilerini kullanın", + "nodeIdTitle": "Düğüm ID", + "secretTitle": "Gizli", + "saveCredentialsTitle": "Kimlik Bilgilerini Yapılandırmaya Ekle", + "saveCredentialsDescription": "Bağlantıyı tamamlamak için bu kimlik bilgilerini öz-host Pangolin düğüm yapılandırma dosyanıza ekleyin.", + "submitButton": "Düğüm Oluştur" + }, + "validation": { + "adoptRequired": "Mevcut bir düğümü benimserken Düğüm ID ve Gizli anahtar gereklidir" + }, + "errors": { + "loadDefaultsFailed": "Varsayılanlar yüklenemedi", + "defaultsNotLoaded": "Varsayılanlar yüklenmedi", + "createFailed": "Düğüm oluşturulamadı" + }, + "success": { + "created": "Düğüm başarıyla oluşturuldu" + } + }, + "remoteExitNodeSelection": "Düğüm Seçimi", + "remoteExitNodeSelectionDescription": "Yerel site için trafiği yönlendirecek düğümü seçin", + "remoteExitNodeRequired": "Yerel siteler için bir düğüm seçilmelidir", + "noRemoteExitNodesAvailable": "Düğüm Bulunamadı", + "noRemoteExitNodesAvailableDescription": "Bu organizasyon için düğüm mevcut değil. Yerel siteleri kullanmak için önce bir düğüm oluşturun.", + "exitNode": "Çıkış Düğümü", + "country": "Ülke", + "rulesMatchCountry": "Şu anda kaynak IP'ye dayanarak", "managedSelfHosted": { "title": "Yönetilen Self-Hosted", "description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi", "willbestoredas": "Şu şekilde depolanacak:", + "roleMappingDescription": "Otomatik Sağlama etkinleştirildiğinde kullanıcıların oturum açarken rollerin nasıl atandığını belirleyin.", + "selectRole": "Bir Rol Seçin", + "roleMappingExpression": "İfade", + "selectRolePlaceholder": "Bir rol seçin", + "selectRoleDescription": "Bu kimlik sağlayıcısından tüm kullanıcılara atanacak bir rol seçin", + "roleMappingExpressionDescription": "Rol bilgilerini ID tokeninden çıkarmak için bir JMESPath ifadesi girin", + "idpTenantIdRequired": "Kiracı Kimliği gereklidir", + "invalidValue": "Geçersiz değer", + "idpTypeLabel": "Kimlik Sağlayıcı Türü", + "roleMappingExpressionPlaceholder": "örn., contains(gruplar, 'yönetici') && 'Yönetici' || 'Üye'", + "idpGoogleConfiguration": "Google Yapılandırması", + "idpGoogleConfigurationDescription": "Google OAuth2 kimlik bilgilerinizi yapılandırın", + "idpGoogleClientIdDescription": "Google OAuth2 İstemci Kimliğiniz", + "idpGoogleClientSecretDescription": "Google OAuth2 İstemci Sırrınız", + "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", + "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", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Google Yapılandırması", + "idpAzureConfigurationTitle": "Azure Entra ID Yapılandırması", + "idpTenantIdLabel": "Kiracı Kimliği", + "idpAzureClientIdDescription2": "Azure Uygulama Kaydı İstemci Kimliğiniz", + "idpAzureClientSecretDescription2": "Azure Uygulama Kaydı İstemci Sırrınız", "idpGoogleDescription": "Google OAuth2/OIDC sağlayıcısı", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı", - "customHeaders": "Özel Başlıklar", - "headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.", + "subnet": "Alt ağ", + "subnetDescription": "Bu organizasyonun ağ yapılandırması için alt ağ.", + "authPage": "Yetkilendirme Sayfası", + "authPageDescription": "Kuruluşunuz için yetkilendirme sayfasını yapılandırın", + "authPageDomain": "Yetkilendirme Sayfası Alanı", + "noDomainSet": "Alan belirlenmedi", + "changeDomain": "Alanı Değiştir", + "selectDomain": "Alan Seçin", + "restartCertificate": "Sertifikayı Yenile", + "editAuthPageDomain": "Yetkilendirme Sayfası Alanını Düzenle", + "setAuthPageDomain": "Yetkilendirme Sayfası Alanını Ayarla", + "failedToFetchCertificate": "Sertifika getirilemedi", + "failedToRestartCertificate": "Sertifika yeniden başlatılamadı", + "addDomainToEnableCustomAuthPages": "Kuruluşunuz için özel kimlik doğrulama sayfalarını etkinleştirmek için bir alan ekleyin", + "selectDomainForOrgAuthPage": "Kuruluşun kimlik doğrulama sayfası için bir alan seçin", "domainPickerProvidedDomain": "Sağlanan Alan Adı", "domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı", "domainPickerVerified": "Doğrulandı", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" {domain} için geçerli yapılamadı.", "domainPickerSubdomainSanitized": "Alt alan adı temizlendi", "domainPickerSubdomainCorrected": "\"{sub}\" \"{sanitized}\" olarak düzeltildi", + "orgAuthSignInTitle": "Kuruluşunuza giriş yapın", + "orgAuthChooseIdpDescription": "Devam etmek için kimlik sağlayıcınızı seçin", + "orgAuthNoIdpConfigured": "Bu kuruluşta yapılandırılmış kimlik sağlayıcı yok. Bunun yerine Pangolin kimliğinizle giriş yapabilirsiniz.", + "orgAuthSignInWithPangolin": "Pangolin ile Giriş Yap", + "subscriptionRequiredToUse": "Bu özelliği kullanmak için abonelik gerekmektedir.", + "idpDisabled": "Kimlik sağlayıcılar devre dışı bırakılmıştır.", + "orgAuthPageDisabled": "Kuruluş kimlik doğrulama sayfası devre dışı bırakılmıştır.", + "domainRestartedDescription": "Alan doğrulaması başarıyla yeniden başlatıldı", "resourceAddEntrypointsEditFile": "Dosyayı düzenle: config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "Dosyayı düzenle: docker-compose.yml" + "resourceExposePortsEditFile": "Dosyayı düzenle: docker-compose.yml", + "emailVerificationRequired": "E-posta doğrulaması gereklidir. Bu adımı tamamlamak için lütfen tekrar {dashboardUrl}/auth/login üzerinden oturum açın. Sonra buraya geri dönün.", + "twoFactorSetupRequired": "İki faktörlü kimlik doğrulama ayarı gereklidir. Bu adımı tamamlamak için lütfen tekrar {dashboardUrl}/auth/login üzerinden oturum açın. Sonra buraya geri dönün.", + "authPageErrorUpdateMessage": "Kimlik doğrulama sayfası ayarları güncellenirken bir hata oluştu.", + "authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi", + "healthCheckNotAvailable": "Yerel", + "rewritePath": "Yolu Yeniden Yaz", + "rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın.", + "continueToApplication": "Uygulamaya Devam Et", + "checkingInvite": "Davet Kontrol Ediliyor", + "setResourceHeaderAuth": "setResourceHeaderAuth", + "resourceHeaderAuthRemove": "Başlık Kimlik Doğrulama Kaldır", + "resourceHeaderAuthRemoveDescription": "Başlık kimlik doğrulama başarıyla kaldırıldı.", + "resourceErrorHeaderAuthRemove": "Başlık Kimlik Doğrulama kaldırılamadı", + "resourceErrorHeaderAuthRemoveDescription": "Kaynak için başlık kimlik doğrulaması kaldırılamadı.", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "Başlık Kimlik Doğrulama ayarlanamadı", + "resourceErrorHeaderAuthSetupDescription": "Kaynak için başlık kimlik doğrulaması ayarlanamadı.", + "resourceHeaderAuthSetup": "Başlık Kimlik Doğrulama başarıyla ayarlandı", + "resourceHeaderAuthSetupDescription": "Başlık kimlik doğrulaması başarıyla ayarlandı.", + "resourceHeaderAuthSetupTitle": "Başlık Kimlik Doğrulama Ayarla", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Başlık Kimlik Doğrulama Ayarla", + "actionSetResourceHeaderAuth": "Başlık Kimlik Doğrulama Ayarla", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "Öncelik", + "priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index c78d7460..508f3f18 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -96,7 +96,7 @@ "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", "siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。", "siteLocalDescription": "仅限本地资源。不需要隧道。", - "siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。", + "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteSeeAll": "查看所有站点", "siteTunnelDescription": "确定如何连接到您的网站", "siteNewtCredentials": "Newt 凭据", @@ -168,6 +168,9 @@ "siteSelect": "选择站点", "siteSearch": "搜索站点", "siteNotFound": "未找到站点。", + "selectCountry": "选择国家", + "searchCountries": "搜索国家...", + "noCountryFound": "找不到国家。", "siteSelectionDescription": "此站点将为目标提供连接。", "resourceType": "资源类型", "resourceTypeDescription": "确定如何访问您的资源", @@ -465,7 +468,10 @@ "createdAt": "创建于", "proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。", "proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。", - "proxyEnableSSL": "启用 SSL (https)", + "proxyEnableSSL": "启用 SSL", + "proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。", + "target": "Target", + "configureTarget": "配置目标", "targetErrorFetch": "获取目标失败", "targetErrorFetchDescription": "获取目标时出错", "siteErrorFetch": "获取资源失败", @@ -492,7 +498,7 @@ "targetTlsSettings": "安全连接配置", "targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置", "targetTlsSettingsAdvanced": "高级TLS设置", - "targetTlsSni": "TLS 服务器名称 (SNI)", + "targetTlsSni": "TLS 服务器名称", "targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。", "targetTlsSubmit": "保存设置", "targets": "目标配置", @@ -501,9 +507,21 @@ "targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。", "methodSelect": "选择方法", "targetSubmit": "添加目标", - "targetNoOne": "没有目标。使用表单添加目标。", + "targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。", "targetNoOneDescription": "在上面添加多个目标将启用负载平衡。", "targetsSubmit": "保存目标", + "addTarget": "添加目标", + "targetErrorInvalidIp": "无效的 IP 地址", + "targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名", + "targetErrorInvalidPort": "无效的端口", + "targetErrorInvalidPortDescription": "请输入有效的端口号", + "targetErrorNoSite": "没有选择站点", + "targetErrorNoSiteDescription": "请选择目标站点", + "targetCreated": "目标已创建", + "targetCreatedDescription": "目标已成功创建", + "targetErrorCreate": "创建目标失败", + "targetErrorCreateDescription": "创建目标时出错", + "save": "保存", "proxyAdditional": "附加代理设置", "proxyAdditionalDescription": "配置你的资源如何处理代理设置", "proxyCustomHeader": "自定义主机标题", @@ -595,7 +613,7 @@ "newtErrorFetchReleases": "无法获取版本信息: {err}", "newtErrorFetchLatest": "无法获取最新版信息: {err}", "newtEndpoint": "Newt 端点", - "newtId": "Newt ID", + "newtId": "Newt ID", "newtSecretKey": "Newt 私钥", "architecture": "架构", "sites": "站点", @@ -712,7 +730,7 @@ "pangolinServerAdmin": "服务器管理员 - Pangolin", "licenseTierProfessional": "专业许可证", "licenseTierEnterprise": "企业许可证", - "licenseTierCommercial": "商业许可证", + "licenseTierPersonal": "Personal License", "licensed": "已授权", "yes": "是", "no": "否", @@ -747,7 +765,7 @@ "idpDisplayName": "此身份提供商的显示名称", "idpAutoProvisionUsers": "自动提供用户", "idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。", - "licenseBadge": "专业版", + "licenseBadge": "EE", "idpType": "提供者类型", "idpTypeDescription": "选择您想要配置的身份提供者类型", "idpOidcConfigure": "OAuth2/OIDC 配置", @@ -914,8 +932,6 @@ "idpConnectingToFinished": "已连接", "idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。", "idpErrorNotFound": "找不到 IdP", - "idpGoogleAlt": "Google", - "idpAzureAlt": "Azure", "inviteInvalid": "无效邀请", "inviteInvalidDescription": "邀请链接无效。", "inviteErrorWrongUser": "邀请不是该用户的", @@ -1083,7 +1099,6 @@ "navbar": "导航菜单", "navbarDescription": "应用程序的主导航菜单", "navbarDocsLink": "文件", - "commercialEdition": "商业版", "otpErrorEnable": "无法启用 2FA", "otpErrorEnableDescription": "启用 2FA 时出错", "otpSetupCheckCode": "请输入您的6位数字代码", @@ -1139,7 +1154,7 @@ "sidebarAllUsers": "所有用户", "sidebarIdentityProviders": "身份提供商", "sidebarLicense": "证书", - "sidebarClients": "客户端(测试版)", + "sidebarClients": "Clients", "sidebarDomains": "域", "enableDockerSocket": "启用 Docker 蓝图", "enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。", @@ -1155,7 +1170,7 @@ "containerLabels": "标签", "containerLabelsCount": "{count, plural, other {# 标签}}", "containerLabelsTitle": "容器标签", - "containerLabelEmpty": "", + "containerLabelEmpty": "<为空>", "containerPorts": "端口", "containerPortsMore": "+{count} 更多", "containerActions": "行动", @@ -1257,6 +1272,48 @@ "domainPickerSubdomain": "子域:{subdomain}", "domainPickerNamespace": "命名空间:{namespace}", "domainPickerShowMore": "显示更多", + "regionSelectorTitle": "选择区域", + "regionSelectorInfo": "选择区域以帮助提升您所在地的性能。您不必与服务器在相同的区域。", + "regionSelectorPlaceholder": "选择一个区域", + "regionSelectorComingSoon": "即将推出", + "billingLoadingSubscription": "正在加载订阅...", + "billingFreeTier": "免费层", + "billingWarningOverLimit": "警告:您已超出一个或多个使用限制。在您修改订阅或调整使用情况之前,您的站点将无法连接。", + "billingUsageLimitsOverview": "使用限制概览", + "billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@fossorial.io。", + "billingDataUsage": "数据使用情况", + "billingOnlineTime": "站点在线时间", + "billingUsers": "活跃用户", + "billingDomains": "活跃域", + "billingRemoteExitNodes": "活跃自托管节点", + "billingNoLimitConfigured": "未配置限制", + "billingEstimatedPeriod": "估计结算周期", + "billingIncludedUsage": "包含的使用量", + "billingIncludedUsageDescription": "您当前订阅计划中包含的使用量", + "billingFreeTierIncludedUsage": "免费层使用额度", + "billingIncluded": "包含", + "billingEstimatedTotal": "预计总额:", + "billingNotes": "备注", + "billingEstimateNote": "这是根据您当前使用情况的估算。", + "billingActualChargesMayVary": "实际费用可能会有变化。", + "billingBilledAtEnd": "您将在结算周期结束时被计费。", + "billingModifySubscription": "修改订阅", + "billingStartSubscription": "开始订阅", + "billingRecurringCharge": "周期性收费", + "billingManageSubscriptionSettings": "管理您的订阅设置和偏好", + "billingNoActiveSubscription": "您没有活跃的订阅。开始订阅以增加使用限制。", + "billingFailedToLoadSubscription": "无法加载订阅", + "billingFailedToLoadUsage": "无法加载使用情况", + "billingFailedToGetCheckoutUrl": "无法获取结账网址", + "billingPleaseTryAgainLater": "请稍后再试。", + "billingCheckoutError": "结账错误", + "billingFailedToGetPortalUrl": "无法获取门户网址", + "billingPortalError": "门户错误", + "billingDataUsageInfo": "当连接到云端时,您将为通过安全隧道传输的所有数据收取费用。 这包括您所有站点的进出流量。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取数据。", + "billingOnlineTimeInfo": "您要根据您的网站连接到云端的时间长短收取费用。 例如,44,640分钟等于一个24/7全月运行的网站。 当您达到上限时,您的站点将断开连接,直到您升级计划或减少使用。使用节点时不收取费用。", + "billingUsersInfo": "根据您组织中的活跃用户数量收费。按日计算账单。", + "billingDomainInfo": "根据组织中活跃域的数量收费。按日计算账单。", + "billingRemoteExitNodesInfo": "根据您组织中已管理节点的数量收费。按日计算账单。", "domainNotFound": "域未找到", "domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。", "failed": "失败", @@ -1290,7 +1347,6 @@ "twoFactorRequired": "注册安全密钥需要两步验证。", "twoFactor": "两步验证", "adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。", - "continueToApplication": "继续到应用程序", "securityKeyAdd": "添加安全密钥", "securityKeyRegisterTitle": "注册新安全密钥", "securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别", @@ -1320,6 +1376,7 @@ "createDomainDnsPropagationDescription": "DNS 更改可能需要一些时间才能在互联网上传播。这可能需要从几分钟到 48 小时,具体取决于您的 DNS 提供商和 TTL 设置。", "resourcePortRequired": "非 HTTP 资源必须输入端口号", "resourcePortNotAllowed": "HTTP 资源不应设置端口号", + "billingPricingCalculatorLink": "价格计算器", "signUpTerms": { "IAgreeToThe": "我同意", "termsOfService": "服务条款", @@ -1346,7 +1403,7 @@ "clientOlmCredentials": "Olm 凭据", "clientOlmCredentialsDescription": "这是 Olm 服务器的身份验证方式", "olmEndpoint": "Olm 端点", - "olmId": "Olm ID", + "olmId": "Olm ID", "olmSecretKey": "Olm 私钥", "clientCredentialsSave": "保存您的凭据", "clientCredentialsSaveDescription": "该信息仅会显示一次,请确保将其复制到安全位置。", @@ -1367,7 +1424,43 @@ "externalProxyEnabled": "外部代理已启用", "addNewTarget": "添加新目标", "targetsList": "目标列表", + "advancedMode": "高级模式", "targetErrorDuplicateTargetFound": "找到重复的目标", + "healthCheckHealthy": "正常", + "healthCheckUnhealthy": "不正常", + "healthCheckUnknown": "未知", + "healthCheck": "健康检查", + "configureHealthCheck": "配置健康检查", + "configureHealthCheckDescription": "为 {target} 设置健康监控", + "enableHealthChecks": "启用健康检查", + "enableHealthChecksDescription": "监视此目标的健康状况。如果需要,您可以监视一个不同的终点。", + "healthScheme": "方法", + "healthSelectScheme": "选择方法", + "healthCheckPath": "路径", + "healthHostname": "IP / 主机", + "healthPort": "端口", + "healthCheckPathDescription": "用于检查健康状态的路径。", + "healthyIntervalSeconds": "正常间隔", + "unhealthyIntervalSeconds": "不正常间隔", + "IntervalSeconds": "正常间隔", + "timeoutSeconds": "超时", + "timeIsInSeconds": "时间以秒为单位", + "retryAttempts": "重试次数", + "expectedResponseCodes": "期望响应代码", + "expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。", + "customHeaders": "自定义标题", + "customHeadersDescription": "头部新行分隔:头部名称:值", + "headersValidationError": "头部必须是格式:头部名称:值。", + "saveHealthCheck": "保存健康检查", + "healthCheckSaved": "健康检查已保存", + "healthCheckSavedDescription": "健康检查配置已成功保存。", + "healthCheckError": "健康检查错误", + "healthCheckErrorDescription": "保存健康检查配置时出错", + "healthCheckPathRequired": "健康检查路径为必填项", + "healthCheckMethodRequired": "HTTP 方法为必填项", + "healthCheckIntervalMin": "检查间隔必须至少为 5 秒", + "healthCheckTimeoutMin": "超时必须至少为 1 秒", + "healthCheckRetryMin": "重试次数必须至少为 1 次", "httpMethod": "HTTP 方法", "selectHttpMethod": "选择 HTTP 方法", "domainPickerSubdomainLabel": "子域名", @@ -1381,6 +1474,7 @@ "domainPickerEnterSubdomainToSearch": "输入一个子域名以搜索并从可用免费域名中选择。", "domainPickerFreeDomains": "免费域名", "domainPickerSearchForAvailableDomains": "搜索可用域名", + "domainPickerNotWorkSelfHosted": "注意:自托管实例当前不提供免费的域名。", "resourceDomain": "域名", "resourceEditDomain": "编辑域名", "siteName": "站点名称", @@ -1463,6 +1557,72 @@ "autoLoginError": "自动登录错误", "autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。", "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。", + "remoteExitNodeManageRemoteExitNodes": "远程节点", + "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud", + "remoteExitNodes": "节点", + "searchRemoteExitNodes": "搜索节点...", + "remoteExitNodeAdd": "添加节点", + "remoteExitNodeErrorDelete": "删除节点时出错", + "remoteExitNodeQuestionRemove": "您确定要从组织中删除 {selectedNode} 节点吗?", + "remoteExitNodeMessageRemove": "一旦删除,该节点将不再能够访问。", + "remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。", + "remoteExitNodeConfirmDelete": "确认删除节点", + "remoteExitNodeDelete": "删除节点", + "sidebarRemoteExitNodes": "远程节点", + "remoteExitNodeCreate": { + "title": "创建节点", + "description": "创建一个新节点来扩展您的网络连接", + "viewAllButton": "查看所有节点", + "strategy": { + "title": "创建策略", + "description": "选择此选项以手动配置您的节点或生成新凭据。", + "adopt": { + "title": "采纳节点", + "description": "如果您已经拥有该节点的凭据,请选择此项。" + }, + "generate": { + "title": "生成密钥", + "description": "如果您想为节点生成新密钥,请选择此选项" + } + }, + "adopt": { + "title": "采纳现有节点", + "description": "输入您想要采用的现有节点的凭据", + "nodeIdLabel": "节点 ID", + "nodeIdDescription": "您想要采用的现有节点的 ID", + "secretLabel": "密钥", + "secretDescription": "现有节点的秘密密钥", + "submitButton": "采用节点" + }, + "generate": { + "title": "生成的凭据", + "description": "使用这些生成的凭据来配置您的节点", + "nodeIdTitle": "节点 ID", + "secretTitle": "密钥", + "saveCredentialsTitle": "将凭据添加到配置中", + "saveCredentialsDescription": "将这些凭据添加到您的自托管 Pangolin 节点配置文件中以完成连接。", + "submitButton": "创建节点" + }, + "validation": { + "adoptRequired": "在通过现有节点时需要节点ID和密钥" + }, + "errors": { + "loadDefaultsFailed": "无法加载默认值", + "defaultsNotLoaded": "默认值未加载", + "createFailed": "创建节点失败" + }, + "success": { + "created": "节点创建成功" + } + }, + "remoteExitNodeSelection": "节点选择", + "remoteExitNodeSelectionDescription": "为此本地站点选择要路由流量的节点", + "remoteExitNodeRequired": "必须为本地站点选择节点", + "noRemoteExitNodesAvailable": "无可用节点", + "noRemoteExitNodesAvailableDescription": "此组织没有可用的节点。首先创建一个节点来使用本地站点。", + "exitNode": "出口节点", + "country": "国家", + "rulesMatchCountry": "当前基于源 IP", "managedSelfHosted": { "title": "托管自托管", "description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器", @@ -1501,10 +1661,53 @@ }, "internationaldomaindetected": "检测到国际域", "willbestoredas": "储存为:", + "roleMappingDescription": "确定当用户启用自动配送时如何分配他们的角色。", + "selectRole": "选择角色", + "roleMappingExpression": "表达式", + "selectRolePlaceholder": "选择角色", + "selectRoleDescription": "选择一个角色,从此身份提供商分配给所有用户", + "roleMappingExpressionDescription": "输入一个 JMESPath 表达式来从 ID 令牌提取角色信息", + "idpTenantIdRequired": "租户ID是必需的", + "invalidValue": "无效的值", + "idpTypeLabel": "身份提供者类型", + "roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'", + "idpGoogleConfiguration": "Google 配置", + "idpGoogleConfigurationDescription": "配置您的 Google OAuth2 凭据", + "idpGoogleClientIdDescription": "您的 Google OAuth2 客户端 ID", + "idpGoogleClientSecretDescription": "您的 Google OAuth2 客户端密钥", + "idpAzureConfiguration": "Azure Entra ID 配置", + "idpAzureConfigurationDescription": "配置您的 Azure Entra ID OAuth2 凭据", + "idpTenantId": "租户 ID", + "idpTenantIdPlaceholder": "您的租户ID", + "idpAzureTenantIdDescription": "您的 Azure 租户ID (在 Azure Active Directory 概览中发现)", + "idpAzureClientIdDescription": "您的 Azure 应用程序注册客户端 ID", + "idpAzureClientSecretDescription": "您的 Azure 应用程序注册客户端密钥", + "idpGoogleTitle": "谷歌", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Google 配置", + "idpAzureConfigurationTitle": "Azure Entra ID 配置", + "idpTenantIdLabel": "租户 ID", + "idpAzureClientIdDescription2": "您的 Azure 应用程序注册客户端 ID", + "idpAzureClientSecretDescription2": "您的 Azure 应用程序注册客户端密钥", "idpGoogleDescription": "Google OAuth2/OIDC 提供商", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", - "customHeaders": "自定义标题", - "headersValidationError": "头部必须是格式:头部名称:值。", + "subnet": "子网", + "subnetDescription": "此组织网络配置的子网。", + "authPage": "认证页面", + "authPageDescription": "配置您的组织认证页面", + "authPageDomain": "认证页面域", + "noDomainSet": "没有域设置", + "changeDomain": "更改域", + "selectDomain": "选择域", + "restartCertificate": "重新启动证书", + "editAuthPageDomain": "编辑认证页面域", + "setAuthPageDomain": "设置认证页面域", + "failedToFetchCertificate": "获取证书失败", + "failedToRestartCertificate": "重新启动证书失败", + "addDomainToEnableCustomAuthPages": "为您的组织添加域名以启用自定义认证页面", + "selectDomainForOrgAuthPage": "选择组织认证页面的域", "domainPickerProvidedDomain": "提供的域", "domainPickerFreeProvidedDomain": "免费提供的域", "domainPickerVerified": "已验证", @@ -1518,6 +1721,177 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 无法为 {domain} 变为有效。", "domainPickerSubdomainSanitized": "子域已净化", "domainPickerSubdomainCorrected": "\"{sub}\" 已被更正为 \"{sanitized}\"", + "orgAuthSignInTitle": "登录到您的组织", + "orgAuthChooseIdpDescription": "选择您的身份提供商以继续", + "orgAuthNoIdpConfigured": "此机构没有配置任何身份提供者。您可以使用您的 Pangolin 身份登录。", + "orgAuthSignInWithPangolin": "使用 Pangolin 登录", + "subscriptionRequiredToUse": "需要订阅才能使用此功能。", + "idpDisabled": "身份提供者已禁用。", + "orgAuthPageDisabled": "组织认证页面已禁用。", + "domainRestartedDescription": "域验证重新启动成功", "resourceAddEntrypointsEditFile": "编辑文件:config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "编辑文件:docker-compose.yml" + "resourceExposePortsEditFile": "编辑文件:docker-compose.yml", + "emailVerificationRequired": "需要电子邮件验证。 请通过 {dashboardUrl}/auth/login 再次登录以完成此步骤。 然后,回到这里。", + "twoFactorSetupRequired": "需要设置双因素身份验证。 请通过 {dashboardUrl}/auth/login 再次登录以完成此步骤。 然后,回到这里。", + "authPageErrorUpdateMessage": "更新身份验证页面设置时出错", + "authPageUpdated": "身份验证页面更新成功", + "healthCheckNotAvailable": "本地的", + "rewritePath": "重写路径", + "rewritePathDescription": "在转发到目标之前,可以选择重写路径。", + "continueToApplication": "继续应用", + "checkingInvite": "正在检查邀请", + "setResourceHeaderAuth": "设置 ResourceHeaderAuth", + "resourceHeaderAuthRemove": "删除头部认证", + "resourceHeaderAuthRemoveDescription": "已成功删除头部身份验证。", + "resourceErrorHeaderAuthRemove": "删除头部身份验证失败", + "resourceErrorHeaderAuthRemoveDescription": "无法删除资源的头部身份验证。", + "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled", + "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled", + "headerAuthRemove": "Remove Header Auth", + "headerAuthAdd": "Add Header Auth", + "resourceErrorHeaderAuthSetup": "设置页眉认证失败", + "resourceErrorHeaderAuthSetupDescription": "无法设置资源的头部身份验证。", + "resourceHeaderAuthSetup": "头部认证设置成功", + "resourceHeaderAuthSetupDescription": "头部认证已成功设置。", + "resourceHeaderAuthSetupTitle": "设置头部身份验证", + "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "设置头部身份验证", + "actionSetResourceHeaderAuth": "设置头部身份验证", + "enterpriseEdition": "Enterprise Edition", + "unlicensed": "Unlicensed", + "beta": "Beta", + "manageClients": "Manage Clients", + "manageClientsDescription": "Clients are devices that can connect to your sites", + "licenseTableValidUntil": "Valid Until", + "saasLicenseKeysSettingsTitle": "Enterprise Licenses", + "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", + "sidebarEnterpriseLicenses": "Licenses", + "generateLicenseKey": "Generate License Key", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "Please enter a valid email address", + "useCaseTypeRequired": "Please select a use case type", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "primaryUseRequired": "Please describe your primary use", + "jobTitleRequiredBusiness": "Job title is required for business use", + "industryRequiredBusiness": "Industry is required for business use", + "stateProvinceRegionRequired": "State/Province/Region is required", + "postalZipCodeRequired": "Postal/ZIP Code is required", + "companyNameRequiredBusiness": "Company name is required for business use", + "countryOfResidenceRequiredBusiness": "Country of residence is required for business use", + "countryRequiredPersonal": "Country is required for personal use", + "agreeToTermsRequired": "You must agree to the terms", + "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "Personal Use", + "description": "For individual, non-commercial use such as learning, personal projects, or experimentation." + }, + "business": { + "title": "Business Use", + "description": "For use within organizations, companies, or commercial or revenue-generating activities." + } + }, + "steps": { + "emailLicenseType": { + "title": "Email & License Type", + "description": "Enter your email and choose your license type" + }, + "personalInformation": { + "title": "Personal Information", + "description": "Tell us about yourself" + }, + "contactInformation": { + "title": "Contact Information", + "description": "Your contact details" + }, + "termsGenerate": { + "title": "Terms & Generate", + "description": "Review and accept terms to generate your license" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "Usage Disclosure", + "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms." + }, + "trialPeriodInformation": { + "title": "Trial Period Information", + "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io." + } + }, + "form": { + "useCaseQuestion": "Are you using Pangolin for personal or business use?", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": "Job Title", + "primaryUseQuestion": "What do you primarily plan to use Pangolin for?", + "industryQuestion": "What is your industry?", + "prospectiveUsersQuestion": "How many prospective users do you expect to have?", + "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", + "companyName": "Company name", + "countryOfResidence": "Country of residence", + "stateProvinceRegion": "State / Province / Region", + "postalZipCode": "Postal / ZIP Code", + "companyWebsite": "Company website", + "companyPhoneNumber": "Company phone number", + "country": "Country", + "phoneNumberOptional": "Phone number (optional)", + "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." + }, + "buttons": { + "close": "Close", + "previous": "Previous", + "next": "Next", + "generateLicenseKey": "Generate License Key" + }, + "toasts": { + "success": { + "title": "License key generated successfully", + "description": "Your license key has been generated and is ready to use." + }, + "error": { + "title": "Failed to generate license key", + "description": "An error occurred while generating the license key." + } + } + }, + "priority": "优先权", + "priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。", + "instanceName": "Instance Name", + "pathMatchModalTitle": "Configure Path Matching", + "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", + "pathMatchType": "Match Type", + "pathMatchPrefix": "Prefix", + "pathMatchExact": "Exact", + "pathMatchRegex": "Regex", + "pathMatchValue": "Path Value", + "clear": "Clear", + "saveChanges": "Save Changes", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/path", + "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", + "pathMatchExactHelp": "Example: /api matches only /api", + "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything", + "pathRewriteModalTitle": "Configure Path Rewriting", + "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.", + "pathRewriteType": "Rewrite Type", + "pathRewritePrefixOption": "Prefix - Replace prefix", + "pathRewriteExactOption": "Exact - Replace entire path", + "pathRewriteRegexOption": "Regex - Pattern replacement", + "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix", + "pathRewriteValue": "Rewrite Value", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "Replace the matched prefix with this value", + "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", + "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", + "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", + "pathRewritePrefix": "Prefix", + "pathRewriteExact": "Exact", + "pathRewriteRegex": "Regex", + "pathRewriteStrip": "Strip", + "pathRewriteStripLabel": "strip" } diff --git a/package-lock.json b/package-lock.json index 098fea0d..460cc451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,11 +32,11 @@ "@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.3", - "@react-email/render": "^1.2.0", + "@react-email/components": "0.5.6", + "@react-email/render": "^1.3.2", "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.1.2", - "@simplewebauthn/server": "^9.0.3", + "@simplewebauthn/browser": "^13.2.2", + "@simplewebauthn/server": "^13.2.2", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -51,9 +51,9 @@ "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "drizzle-orm": "0.44.5", - "eslint": "9.35.0", - "eslint-config-next": "15.5.3", + "drizzle-orm": "0.44.6", + "eslint": "9.37.0", + "eslint-config-next": "15.5.4", "express": "5.1.0", "express-rate-limit": "8.1.0", "glob": "11.0.3", @@ -64,31 +64,33 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", + "maxmind": "5.0.0", "moment": "2.30.1", - "next": "15.5.3", - "next-intl": "^4.3.9", + "next": "15.5.4", + "next-intl": "^4.3.12", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.6", - "npm": "^11.6.0", + "nodemailer": "7.0.9", + "npm": "^11.6.2", "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "react": "19.1.1", - "react-dom": "19.1.1", - "react-easy-sort": "^1.7.0", - "react-hook-form": "7.62.0", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-easy-sort": "^1.8.0", + "react-hook-form": "7.65.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", - "semver": "^7.7.2", + "resend": "^6.1.2", + "semver": "^7.7.3", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", "tw-animate-css": "^1.3.8", "uuid": "^13.0.0", "vaul": "1.1.2", - "winston": "3.17.0", + "winston": "3.18.3", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", "yargs": "18.0.0", @@ -96,9 +98,10 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.49.1", + "@dotenvx/dotenvx": "1.51.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "^4.1.13", + "@react-email/preview-server": "4.3.0", + "@tailwindcss/postcss": "^4.1.14", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", @@ -108,25 +111,25 @@ "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.5.2", - "@types/nodemailer": "7.0.1", + "@types/node": "24.7.2", + "@types/nodemailer": "7.0.2", "@types/pg": "8.15.5", - "@types/react": "19.1.13", - "@types/react-dom": "19.1.9", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.1", "@types/semver": "^7.7.1", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.4", + "drizzle-kit": "0.31.5", "esbuild": "0.25.10", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.2.11", + "react-email": "4.3.0", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", - "tsx": "4.20.5", + "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.44.0" + "typescript-eslint": "^8.46.0" } }, "node_modules/@alloc/quick-lru": { @@ -142,6 +145,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asteasolutions/zod-to-openapi": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.4.tgz", @@ -290,557 +307,530 @@ } }, "node_modules/@aws-sdk/client-sesv2": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.888.0.tgz", - "integrity": "sha512-Zy7AXvj4oVLE5Zkj61qYZxIFgJXbRgTmFJvQ/EqgxE87KPR9+gF5wtC3iqcKEmkqFlWlxWrlhV4K70Vqqj4bZQ==", + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.910.0.tgz", + "integrity": "sha512-YKCXrzbEhplwsvjAnMKUB9lAfawFHMz7tPLL3dCnKQYeZOQqsYiUUPjkiB2Vq8uV+ALZqJ2FCph07pW7XZHqjg==", "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.888.0", - "@aws-sdk/credential-provider-node": "3.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", - "@aws-sdk/signature-v4-multi-region": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", - "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", - "@smithy/core": "^3.11.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/credential-provider-node": "3.910.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.910.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/signature-v4-multi-region": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.910.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@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.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.888.0.tgz", - "integrity": "sha512-8CLy/ehGKUmekjH+VtZJ4w40PqDg3u0K7uPziq/4P8Q7LLgsy8YQoHNbuY4am7JU3HWrqLXJI9aaz1+vPGPoWA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.910.0.tgz", + "integrity": "sha512-oEWXhe2RHiSPKxhrq1qp7M4fxOsxMIJc4d75z8tTLLm5ujlmTZYU3kd0l2uBBaZSlbkrMiefntT6XrGint1ibw==", "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.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", - "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", - "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", - "@smithy/core": "^3.11.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.910.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.910.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@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.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.888.0.tgz", - "integrity": "sha512-L3S2FZywACo4lmWv37Y4TbefuPJ1fXWyWwIJ3J4wkPYFJ47mmtUPqThlVrSbdTHkEjnZgJe5cRfxk0qCLsFh1w==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.910.0.tgz", + "integrity": "sha512-b/FVNyPxZMmBp+xDwANDgR6o5Ehh/RTY9U/labH56jJpte196Psru/FmQULX3S6kvIiafQA9JefWUq81SfWVLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", - "@aws-sdk/xml-builder": "3.887.0", - "@smithy/core": "^3.11.0", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", - "fast-xml-parser": "5.2.5", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/xml-builder": "3.910.0", + "@smithy/core": "^3.16.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.888.0.tgz", - "integrity": "sha512-shPi4AhUKbIk7LugJWvNpeZA8va7e5bOHAEKo89S0Ac8WDZt2OaNzbh/b9l0iSL2eEyte8UgIsYGcFxOwIF1VA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.910.0.tgz", + "integrity": "sha512-Os8I5XtTLBBVyHJLxrEB06gSAZeFMH2jVoKhAaFybjOTiV7wnjBgjvWjRfStnnXs7p9d+vc/gd6wIZHjony5YQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.888.0.tgz", - "integrity": "sha512-Jvuk6nul0lE7o5qlQutcqlySBHLXOyoPtiwE6zyKbGc7RVl0//h39Lab7zMeY2drMn8xAnIopL4606Fd8JI/Hw==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.910.0.tgz", + "integrity": "sha512-3KiGsTlqMnvthv90K88Uv3SvaUbmcTShBIVWYNaHdbrhrjVRR08dm2Y6XjQILazLf1NPFkxUou1YwCWK4nae1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.1", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/property-provider": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-stream": "^4.5.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.888.0.tgz", - "integrity": "sha512-M82ItvS5yq+tO6ZOV1ruaVs2xOne+v8HW85GFCXnz8pecrzYdgxh6IsVqEbbWruryG/mUGkWMbkBZoEsy4MgyA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.910.0.tgz", + "integrity": "sha512-/8x9LKKaLGarvF1++bFEFdIvd9/djBb+HTULbJAf4JVg3tUlpHtGe7uquuZaQkQGeW4XPbcpB9RMWx5YlZkw3w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/credential-provider-env": "3.888.0", - "@aws-sdk/credential-provider-http": "3.888.0", - "@aws-sdk/credential-provider-process": "3.888.0", - "@aws-sdk/credential-provider-sso": "3.888.0", - "@aws-sdk/credential-provider-web-identity": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/credential-provider-env": "3.910.0", + "@aws-sdk/credential-provider-http": "3.910.0", + "@aws-sdk/credential-provider-process": "3.910.0", + "@aws-sdk/credential-provider-sso": "3.910.0", + "@aws-sdk/credential-provider-web-identity": "3.910.0", + "@aws-sdk/nested-clients": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.888.0.tgz", - "integrity": "sha512-KCrQh1dCDC8Y+Ap3SZa6S81kHk+p+yAaOQ5jC3dak4zhHW3RCrsGR/jYdemTOgbEGcA6ye51UbhWfrrlMmeJSA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.910.0.tgz", + "integrity": "sha512-Zz5tF/U4q9ir3rfVnPLlxbhMTHjPaPv78TarspFYn9mNN7cPVXBaXVVnMNu6ypZzBdTB8M44UYo827Qcw3kouA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.888.0", - "@aws-sdk/credential-provider-http": "3.888.0", - "@aws-sdk/credential-provider-ini": "3.888.0", - "@aws-sdk/credential-provider-process": "3.888.0", - "@aws-sdk/credential-provider-sso": "3.888.0", - "@aws-sdk/credential-provider-web-identity": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/credential-provider-env": "3.910.0", + "@aws-sdk/credential-provider-http": "3.910.0", + "@aws-sdk/credential-provider-ini": "3.910.0", + "@aws-sdk/credential-provider-process": "3.910.0", + "@aws-sdk/credential-provider-sso": "3.910.0", + "@aws-sdk/credential-provider-web-identity": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.888.0.tgz", - "integrity": "sha512-+aX6piSukPQ8DUS4JAH344GePg8/+Q1t0+kvSHAZHhYvtQ/1Zek3ySOJWH2TuzTPCafY4nmWLcQcqvU1w9+4Lw==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.910.0.tgz", + "integrity": "sha512-l1lZfHIl/z0SxXibt7wMQ2HmRIyIZjlOrT6a554xlO//y671uxPPwScVw7QW4fPIvwfmKbl8dYCwGI//AgQ0bA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.888.0.tgz", - "integrity": "sha512-b1ZJji7LJ6E/j1PhFTyvp51in2iCOQ3VP6mj5H6f5OUnqn7efm41iNMoinKr87n0IKZw7qput5ggXVxEdPhouA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.910.0.tgz", + "integrity": "sha512-cwc9bmomjUqPDF58THUCmEnpAIsCFV3Y9FHlQmQbMkYUm7Wlrb5E2iFrZ4WDefAHuh25R/gtj+Yo74r3gl9kbw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.888.0", - "@aws-sdk/core": "3.888.0", - "@aws-sdk/token-providers": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/client-sso": "3.910.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/token-providers": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.888.0.tgz", - "integrity": "sha512-7P0QNtsDzMZdmBAaY/vY1BsZHwTGvEz3bsn2bm5VSKFAeMmZqsHK1QeYdNsFjLtegnVh+wodxMq50jqLv3LFlA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.910.0.tgz", + "integrity": "sha512-HFQgZm1+7WisJ8tqcZkNRRmnoFO+So+L12wViVxneVJ+OclfL2vE/CoKqHTozP6+JCOKMlv6Vi61Lu6xDtKdTA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/nested-clients": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.887.0.tgz", - "integrity": "sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.910.0.tgz", + "integrity": "sha512-F9Lqeu80/aTM6S/izZ8RtwSmjfhWjIuxX61LX+/9mxJyEkgaECRxv0chsLQsLHJumkGnXRy/eIyMLBhcTPF5vg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.910.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.887.0.tgz", - "integrity": "sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.910.0.tgz", + "integrity": "sha512-3LJyyfs1USvRuRDla1pGlzGRtXJBXD1zC9F+eE9Iz/V5nkmhyv52A017CvKWmYoR0DM9dzjLyPOI0BSSppEaTw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.887.0.tgz", - "integrity": "sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.910.0.tgz", + "integrity": "sha512-m/oLz0EoCy+WoIVBnXRXJ4AtGpdl0kPE7U+VH9TsuUzHgxY1Re/176Q1HWLBRVlz4gr++lNsgsMWEC+VnAwMpw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", + "@aws-sdk/types": "3.910.0", "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.888.0.tgz", - "integrity": "sha512-rKOFNfqgqOfrdcLGF8fcO75azWS2aq2ksRHFoIEFru5FJxzu/yDAhY4C2FKiP/X34xeIUS2SbE/gQgrgWHSN2g==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.910.0.tgz", + "integrity": "sha512-m13TmWHjIonWkIFi4O1GSsZKPzIf2sO9rUEj9fr1VwTA7Lblp6UaOcfiQHfhWXgxqYaSouvEvCtoqA3SttdPlw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-arn-parser": "3.873.0", - "@smithy/core": "^3.11.0", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.16.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-stream": "^4.5.2", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.888.0.tgz", - "integrity": "sha512-ZkcUkoys8AdrNNG7ATjqw2WiXqrhTvT+r4CIK3KhOqIGPHX0p0DQWzqjaIl7ZhSUToKoZ4Ud7MjF795yUr73oA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.910.0.tgz", + "integrity": "sha512-djpnECwDLI/4sck1wxK/cZJmZX5pAhRvjONyJqr0AaOfJyuIAG0PHLe7xwCrv2rCAvIBR9ofnNFzPIGTJPDUwg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", - "@smithy/core": "^3.11.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@smithy/core": "^3.16.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.888.0.tgz", - "integrity": "sha512-py4o4RPSGt+uwGvSBzR6S6cCBjS4oTX5F8hrHFHfPCdIOMVjyOBejn820jXkCrcdpSj3Qg1yUZXxsByvxc9Lyg==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.910.0.tgz", + "integrity": "sha512-Jr/smgVrLZECQgMyP4nbGqgJwzFFbkjOVrU8wh/gbVIZy1+Gu6R7Shai7KHDkEjwkGcHpN1MCCO67jTAOoSlMw==", "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.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", - "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", - "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", - "@smithy/core": "^3.11.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.910.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.910.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@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.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.887.0.tgz", - "integrity": "sha512-VdSMrIqJ3yjJb/fY+YAxrH/lCVv0iL8uA+lbMNfQGtO5tB3Zx6SU9LEpUwBNX8fPK1tUpI65CNE4w42+MY/7Mg==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.910.0.tgz", + "integrity": "sha512-gzQAkuHI3xyG6toYnH/pju+kc190XmvnB7X84vtN57GjgdQJICt9So/BD0U6h+eSfk9VBnafkVrAzBzWMEFZVw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.1.1", + "@aws-sdk/types": "3.910.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.888.0.tgz", - "integrity": "sha512-FmOHUaJzEhqfcpyh0L7HLwYcYopK13Dbmuf+oUyu56/RoeB1nLnltH1VMQVj8v3Am2IwlGR+/JpFyrdkErN+cA==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.910.0.tgz", + "integrity": "sha512-SM62pR9ozCNzbKuV315QSgR1Tkyy+0sKMzgGAufvOupuWBUaJgEuzCwfLEBhPiEODaQCdJ3UZGn0wYXxn8gXsA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.1.3", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-sdk-s3": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.888.0.tgz", - "integrity": "sha512-WA3NF+3W8GEuCMG1WvkDYbB4z10G3O8xuhT7QSjhvLYWQ9CPt3w4VpVIfdqmUn131TCIbhCzD0KN/1VJTjAjyw==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.910.0.tgz", + "integrity": "sha512-dQr3pFpzemKyrB7SEJ2ipPtWrZiL5vaimg2PkXpwyzGrigYRc8F2R9DMUckU5zi32ozvQqq4PI3bOrw6xUfcbQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.910.0", + "@aws-sdk/nested-clients": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/types": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.887.0.tgz", - "integrity": "sha512-fmTEJpUhsPsovQ12vZSpVTEP/IaRoJAMBGQXlQNjtCpkBp6Iq3KQDa/HDaPINE+3xxo6XvTdtibsNOd5zJLV9A==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.910.0.tgz", + "integrity": "sha512-o67gL3vjf4nhfmuSUNNkit0d62QJEwwHLxucwVJkR/rw9mfUtAWsgBs8Tp16cdUbMgsyQtCQilL8RAJDoGtadQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", - "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.910.0.tgz", + "integrity": "sha512-6XgdNe42ibP8zCQgNGDWoOF53RfEKzpU/S7Z29FTTJ7hcZv0SytC0ZNQQZSx4rfBl036YWYwJRoJMlT4AA7q9A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-endpoints": "^3.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.887.0.tgz", - "integrity": "sha512-kpegvT53KT33BMeIcGLPA65CQVxLUL/C3gTz9AzlU/SDmeusBHX4nRApAicNzI/ltQ5lxZXbQn18UczzBuwF1w==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.910.0.tgz", + "integrity": "sha512-iOdrRdLZHrlINk9pezNZ82P/VxO/UmtmpaOAObUN+xplCUJu31WNM2EE/HccC8PQw6XlAudpdA6HDTGiW6yVGg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.887.0", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", - "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.887.0.tgz", - "integrity": "sha512-X71UmVsYc6ZTH4KU6hA5urOzYowSXc3qvroagJNLJYU1ilgZ529lP4J9XOYfEvTXkLR1hPFSRxa43SrwgelMjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.887.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.888.0.tgz", - "integrity": "sha512-rSB3OHyuKXotIGfYEo//9sU0lXAUrTY28SUUnxzOGYuQsAt0XR5iYwBAp+RjV6x8f+Hmtbg0PdCsy1iNAXa0UQ==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.910.0.tgz", + "integrity": "sha512-qNV+rywWQDOOWmGpNlWLCU6zkJurocTBB2uLSdQ8b6Xg6U/i1VTJsoUQ5fbhSQpp/SuBGiIglyB1gSc0th7hPw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/types": "3.887.0", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-user-agent": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -855,14 +845,55 @@ } } }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.887.0.tgz", - "integrity": "sha512-lMwgWK1kNgUhHGfBvO/5uLe7TKhycwOn3eRCqsKPT9aPCx/HWuTlpcQp8oW2pCRGLS7qzcxqpQulcD+bbUL7XQ==", + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.910.0.tgz", + "integrity": "sha512-UK0NzRknzUITYlkDibDSgkWvhhC11OLhhhGajl6pYCACup+6QE4SsLvmAGMkyNtGVCJ6Q+BM6PwDCBZyBgwl9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.7.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.901.0.tgz", + "integrity": "sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -894,15 +925,66 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -911,6 +993,49 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -921,6 +1046,108 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -941,14 +1168,38 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -972,29 +1223,45 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1015,20 +1282,20 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "license": "MIT", "dependencies": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.49.1.tgz", - "integrity": "sha512-LQ8cem3RU/mI2iz5Sy+ypnhfhVge3bc9tsLJg5rdf7j7u1RRTfmmSdLwSjeYI7sL9ToN7rgFkOGSBJqaBT+gSQ==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", + "integrity": "sha512-CbMGzyOYSyFF7d4uaeYwO9gpSBzLTnMmSmTVpCZjvpJFV69qYbjYPpzNnCz1mb2wIvEhjWjRwQWuBzTO0jITww==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1072,20 +1339,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.4", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, "dependencies": { @@ -1093,9 +1360,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2050,18 +2317,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2093,10 +2363,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2115,12 +2397,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -2137,9 +2419,9 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", - "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", @@ -2147,12 +2429,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", - "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.3" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -2166,21 +2448,21 @@ "license": "MIT" }, "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", - "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.1", + "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", - "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", "license": "MIT", "dependencies": { "tslib": "^2.8.0" @@ -2196,23 +2478,23 @@ } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", - "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", - "@formatjs/icu-skeleton-parser": "1.8.14", + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", - "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, @@ -2235,6 +2517,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" }, @@ -2252,31 +2535,18 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2303,13 +2573,24 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "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", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2322,16 +2603,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "@img/sharp-libvips-darwin-arm64": "1.2.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "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": [ @@ -2344,16 +2626,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2364,12 +2647,13 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2380,12 +2664,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2396,12 +2681,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2412,12 +2698,13 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2428,12 +2715,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2444,12 +2732,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2460,12 +2749,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2476,12 +2766,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "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" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2492,12 +2783,13 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2510,16 +2802,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" + "@img/sharp-libvips-linux-arm": "1.2.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2532,16 +2825,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" + "@img/sharp-libvips-linux-arm64": "1.2.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2554,16 +2848,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" + "@img/sharp-libvips-linux-ppc64": "1.2.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2576,16 +2871,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" + "@img/sharp-libvips-linux-s390x": "1.2.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2598,16 +2894,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" + "@img/sharp-libvips-linux-x64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2620,16 +2917,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "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" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2642,20 +2940,21 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "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" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.4" + "@emnapi/runtime": "^1.5.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2665,12 +2964,13 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "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" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2684,12 +2984,13 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "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" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2703,12 +3004,13 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "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" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2804,6 +3106,17 @@ "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", @@ -2812,9 +3125,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -2828,6 +3141,26 @@ "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/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2841,24 +3174,24 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", + "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", - "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.4.tgz", + "integrity": "sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz", + "integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==", "cpu": [ "arm64" ], @@ -2872,9 +3205,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", + "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", "cpu": [ "x64" ], @@ -2888,9 +3221,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz", + "integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==", "cpu": [ "arm64" ], @@ -2904,9 +3237,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz", + "integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==", "cpu": [ "arm64" ], @@ -2920,9 +3253,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz", + "integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==", "cpu": [ "x64" ], @@ -2936,9 +3269,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz", + "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==", "cpu": [ "x64" ], @@ -2952,9 +3285,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz", + "integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==", "cpu": [ "arm64" ], @@ -2968,9 +3301,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz", + "integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==", "cpu": [ "x64" ], @@ -2997,9 +3330,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", - "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "dev": true, "license": "MIT", "dependencies": { @@ -3651,44 +3984,111 @@ "license": "MIT" }, "node_modules/@peculiar/asn1-android": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.4.0.tgz", - "integrity": "sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.5.0.tgz", + "integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.5.0.tgz", + "integrity": "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.5.0.tgz", + "integrity": "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.4.0.tgz", - "integrity": "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz", + "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.4.0", - "@peculiar/asn1-x509": "^2.4.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.5.0.tgz", + "integrity": "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.5.0.tgz", + "integrity": "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.5.0.tgz", + "integrity": "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pfx": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.4.0.tgz", - "integrity": "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz", + "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.4.0", - "@peculiar/asn1-x509": "^2.4.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz", - "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz", + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", "license": "MIT", "dependencies": { "asn1js": "^3.0.6", @@ -3697,17 +4097,66 @@ } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.4.0.tgz", - "integrity": "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz", + "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.5.0.tgz", + "integrity": "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "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==", + "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", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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", @@ -4558,6 +5007,62 @@ } } }, + "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-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -4836,9 +5341,9 @@ } }, "node_modules/@react-email/components": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.3.tgz", - "integrity": "sha512-8G5vsoMehuGOT4cDqaYLdpagtqCYPl4vThXNylClxO6SrN2w9Mh1+i2RNGj/rdqh/woamHORjlXMYCA/kzDMew==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.6.tgz", + "integrity": "sha512-3o9ellDaF3bBcVMWeos9HI0iUIT1zGygPRcn9WSfI5JREORiN6ViEJIvz5SKWEn1KPNZtw/iaW8ct7PpVyhomg==", "license": "MIT", "dependencies": { "@react-email/body": "0.1.0", @@ -4856,7 +5361,7 @@ "@react-email/link": "0.0.12", "@react-email/markdown": "0.0.15", "@react-email/preview": "0.0.13", - "@react-email/render": "1.2.3", + "@react-email/render": "1.3.2", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", "@react-email/tailwind": "1.2.2", @@ -4989,10 +5494,534 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/preview-server": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.0.tgz", + "integrity": "sha512-cUaSrxezCzdg2hF6PzIxVrtagLdw3z3ovHeB3y2RDkmDZpp7EeIoNyJm22Ch2S0uAqTZNAgqu67aroLn3mFC1A==", + "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/@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-darwin-arm64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-x64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@next/swc-linux-x64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/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==", + "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.2.3", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.2.3.tgz", - "integrity": "sha512-qu3XYNkHGao3teJexVD5CrcgFkNLrzbZvpZN17a7EyQYUN3kHkTkE9saqY4VbvGx6QoNU3p8rsk/Xm++D/+pTw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.3.2.tgz", + "integrity": "sha512-oq8/BD/I/YspeuBjjdLJG6xaf9tsPYk+VWu8/mX9xWbRN0t0ExKSVm9sEBL6RsCpndQA2jbY2VgPEreIrzUgqw==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", @@ -5062,9 +6091,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz", + "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==", "license": "MIT" }, "node_modules/@scarf/scarf": { @@ -5094,15 +6123,15 @@ } }, "node_modules/@simplewebauthn/browser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.2.tgz", - "integrity": "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", + "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", "license": "MIT" }, "node_modules/@simplewebauthn/server": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.3.tgz", - "integrity": "sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz", + "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", @@ -5112,28 +6141,20 @@ "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", - "@simplewebauthn/types": "^9.0.1", - "cross-fetch": "^4.0.0" + "@peculiar/x509": "^1.13.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, - "node_modules/@simplewebauthn/types": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz", - "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, "node_modules/@smithy/abort-controller": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", - "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.2.tgz", + "integrity": "sha512-fPbcmEI+A6QiGOuumTpKSo7z+9VYr5DLN8d5/8jDJOwmt4HAKy/UGuRstCMpKbtr+FMaHH4pvFinSAbIAYCHZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5141,16 +6162,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.1.tgz", - "integrity": "sha512-FXil8q4QN7mgKwU2hCLm0ltab8NyY/1RiqEf25Jnf6WLS3wmb11zGAoLETqg1nur2Aoibun4w4MjeN9CMJ4G6A==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.2.tgz", + "integrity": "sha512-F/G+VaulIebINyfvcoXmODgIc7JU/lxWK9/iI0Divxyvd2QWB7/ZcF7JKwMssWI6/zZzlMkq/Pt6ow2AOEebPw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5158,53 +6179,38 @@ } }, "node_modules/@smithy/core": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", - "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.16.1.tgz", + "integrity": "sha512-yRx5ag3xEQ/yGvyo80FVukS7ZkeUP49Vbzg0MjfHLkuCIgg5lFtaEJfZR178KJmjWPqLU4d0P4k7SKgF9UkOaQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.1", - "@smithy/util-utf8": "^4.1.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/middleware-serde": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-stream": "^4.5.2", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/core/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.1.tgz", - "integrity": "sha512-1WdBfM9DwA59pnpIizxnUvBf/de18p4GP+6zP2AqrlFzoW3ERpZaT4QueBR0nS9deDMaQRkBlngpVlnkuuTisQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.2.tgz", + "integrity": "sha512-hOjFTK+4mfehDnfjNkPqHUKBKR2qmlix5gy7YzruNbTdeoBE3QkfNCPvuCK2r05VUJ02QQ9bz2G41CxhSexsMw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", - "@smithy/property-provider": "^4.1.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5212,16 +6218,16 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", - "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.3.tgz", + "integrity": "sha512-cipIcM3xQ5NdIVwcRb37LaQwIxZNMEZb/ZOPmLFS9uGo9TGx2dGCyMBj9oT7ypH4TUD/kOTc/qHmwQzthrSk+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/querystring-builder": "^4.1.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/querystring-builder": "^4.2.2", + "@smithy/types": "^4.7.1", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5229,15 +6235,15 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", - "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.2.tgz", + "integrity": "sha512-xuOPGrF2GUP+9og5NU02fplRVjJjMhAaY8ZconB3eLKjv/VSV9/s+sFf72MYO5Q2jcSRVk/ywZHpyGbE3FYnFQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/types": "^4.7.1", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5245,13 +6251,13 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", - "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.2.tgz", + "integrity": "sha512-Z0844Zpoid5L1DmKX2+cn2Qu9i3XWjhzwYBRJEWrKJwjUuhEkzf37jKPj9dYFsZeKsAbS2qI0JyLsYafbXJvpA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5259,9 +6265,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", - "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5272,14 +6278,14 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", - "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.2.tgz", + "integrity": "sha512-aJ7LAuIXStF6EqzRVX9kAW+6/sYoJJv0QqoFrz2BhA9r/85kLYOJ6Ph47wYSGBxzSLxsYT5jqgMw/qpbv1+m+w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5287,19 +6293,19 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.1.tgz", - "integrity": "sha512-fUTMmQvQQZakXOuKizfu7fBLDpwvWZjfH6zUK2OLsoNZRZGbNUdNSdLJHpwk1vS208jtDjpUIskh+JoA8zMzZg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.3.tgz", + "integrity": "sha512-CfxQ6X9L87/3C67Po6AGWXsx8iS4w2BO8vQEZJD6hwqg2vNRC/lMa2O5wXYCG9tKotdZ0R8KG33TS7kpUnYKiw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.11.0", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/shared-ini-file-loader": "^4.1.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-middleware": "^4.1.1", + "@smithy/core": "^3.16.1", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-middleware": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5307,50 +6313,35 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.1.tgz", - "integrity": "sha512-JzfvjwSJXWRl7LkLgIRTUTd2Wj639yr3sQGpViGNEOjtb0AkAuYqRAHs+jSOI/LPC0ZTjmFVVtfrCICMuebexw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.3.tgz", + "integrity": "sha512-EHnKGeFuzbmER4oSl/VJDxPLi+aiZUb3nk5KK8eNwHjMhI04jHlui2ZkaBzMfNmXOgymaS6zV//fyt6PSnI1ow==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/service-error-classification": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/node-config-provider": "^4.3.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/service-error-classification": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", - "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.2.tgz", + "integrity": "sha512-tDMPMBCsA1GBxanShhPvQYwdiau3NmctUp+eELMhUTDua+EUrugXlaKCnTMMoEB5mbHFebdv81uJPkVP02oihA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5358,13 +6349,13 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", - "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.2.tgz", + "integrity": "sha512-7rgzDyLOQouh1bC6gOXnCGSX2dqvbOclgClsFkj735xQM2CHV63Ams8odNZGJgcqnBsEz44V/pDGHU6ALEUD+w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5372,15 +6363,15 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.1.tgz", - "integrity": "sha512-AIA0BJZq2h295J5NeCTKhg1WwtdTA/GqBCaVjk30bDgMHwniUETyh5cP9IiE9VrId7Kt8hS7zvREVMTv1VfA6g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.2.tgz", + "integrity": "sha512-u38G0Audi2ORsL0QnzhopZ3yweMblQf8CZNbzUJ3wfTtZ7OiOwOzee0Nge/3dKeG/8lx0kt8K0kqDi6sYu0oKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5388,16 +6379,16 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", - "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.1.tgz", + "integrity": "sha512-9gKJoL45MNyOCGTG082nmx0A6KrbLVQ+5QSSKyzRi0AzL0R81u3wC1+nPvKXgTaBdAKM73fFPdCBHpmtipQwdQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/querystring-builder": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/abort-controller": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/querystring-builder": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5405,13 +6396,13 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", - "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.2.tgz", + "integrity": "sha512-MW7MfI+qYe/Ue5RH0uEztEKB+vBlOMM+1Dz68qzTsY8fC9kanXMFPEVdiq35JTGKWt5wZAjU1R0uXYEjK2MM1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5419,13 +6410,13 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", - "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.2.tgz", + "integrity": "sha512-nkKOI8xEkBXUmdxsFExomOb+wkU+Xgn0Fq2LMC7YIX5r4YPUg7PLayV/s/u3AtbyjWYlrvN7nAiDTLlqSdUjHw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5433,14 +6424,14 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", - "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.2.tgz", + "integrity": "sha512-YgXvq89o+R/8zIoeuXYv8Ysrbwgjx+iVYu9QbseqZjMDAhIg/FRt7jis0KASYFtd/Cnsnz4/nYTJXkJDWe8wHg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-uri-escape": "^4.1.0", + "@smithy/types": "^4.7.1", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5448,13 +6439,13 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", - "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.2.tgz", + "integrity": "sha512-DczOD2yJy3NXcv1JvhjFC7bIb/tay6nnIRD/qrzBaju5lrkVBOwCT3Ps37tra20wy8PicZpworStK7ZcI9pCRQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5462,26 +6453,26 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", - "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.2.tgz", + "integrity": "sha512-1X17cMLwe/vb4RpZbQVpJ1xQQ7fhQKggMdt3qjdV3+6QNllzvUXyS3WFnyaFWLyaGqfYHKkNONbO1fBCMQyZtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0" + "@smithy/types": "^4.7.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.1.1.tgz", - "integrity": "sha512-YkpikhIqGc4sfXeIbzSj10t2bJI/sSoP5qxLue6zG+tEE3ngOBSm8sO3+djacYvS/R5DfpxN/L9CyZsvwjWOAQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.2.tgz", + "integrity": "sha512-AWnLgSmOTdDXM8aZCN4Im0X07M3GGffeL9vGfea4mdKZD0cPT9yLF9SsRbEa00tHLI+KfubDrmjpaKT2pM4GdQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5489,19 +6480,19 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", - "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.2.tgz", + "integrity": "sha512-BRnQGGyaRSSL0KtjjFF9YoSSg8qzSqHMub4H2iKkd+LZNzZ1b7H5amslZBzi+AnvuwPMyeiNv0oqay/VmIuoRA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.1.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-hex-encoding": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-uri-escape": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5509,18 +6500,18 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.1.tgz", - "integrity": "sha512-WolVLDb9UTPMEPPOncrCt6JmAMCSC/V2y5gst2STWJ5r7+8iNac+EFYQnmvDCYMfOLcilOSEpm5yXZXwbLak1Q==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.8.1.tgz", + "integrity": "sha512-N5wK57pVThzLVK5NgmHxocTy5auqGDGQ+JsL5RjCTriPt8JLYgXT0Awa915zCpzc9hXHDOKqDX5g9BFdwkSfUA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.11.0", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.1", + "@smithy/core": "^3.16.1", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-stream": "^4.5.2", "tslib": "^2.6.2" }, "engines": { @@ -5528,9 +6519,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", - "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.7.1.tgz", + "integrity": "sha512-WwP7vzoDyzvIFLzF5UhLQ6AsEx/PvSObzlNtJNW3lLy+BaSvTqCU628QKVvcJI/dydlAS1mSHQP7anKcxDcOxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5541,14 +6532,14 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", - "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.2.tgz", + "integrity": "sha512-s2EYKukaswzjiHJCss6asB1F4zjRc0E/MFyceAKzb3+wqKA2Z/+Gfhb5FP8xVVRHBAvBkregaQAydifgbnUlCw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/querystring-parser": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5556,14 +6547,14 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", - "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5571,9 +6562,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", - "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5584,9 +6575,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", - "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5597,13 +6588,13 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", - "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.1.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5611,9 +6602,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", - "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5624,16 +6615,15 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.1.tgz", - "integrity": "sha512-hA1AKIHFUMa9Tl6q6y8p0pJ9aWHCCG8s57flmIyLE0W7HcJeYrYtnqXDcGnftvXEhdQnSexyegXnzzTGk8bKLA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.2.tgz", + "integrity": "sha512-6JvKHZ5GORYkEZ2+yJKEHp6dQQKng+P/Mu3g3CDy0fRLQgXEO8be+FLrBGGb4kB9lCW6wcQDkN7kRiGkkVAXgg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5641,18 +6631,18 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.1.tgz", - "integrity": "sha512-RGSpmoBrA+5D2WjwtK7tto6Pc2wO9KSXKLpLONhFZ8VyuCbqlLdiDAfuDTNY9AJe4JoE+Cx806cpTQQoQ71zPQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.3.tgz", + "integrity": "sha512-bkTGuMmKvghfCh9NayADrQcjngoF8P+XTgID5r3rm+8LphFiuM6ERqpBS95YyVaLjDetnKus9zK/bGlkQOOtNQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.2.1", - "@smithy/credential-provider-imds": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", - "@smithy/types": "^4.5.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5660,14 +6650,14 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.1.tgz", - "integrity": "sha512-qB4R9kO0SetA11Rzu6MVGFIaGYX3p6SGGGfWwsKnC6nXIf0n/0AKVwRTsYsz9ToN8CeNNtNgQRwKFBndGJZdyw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.2.tgz", + "integrity": "sha512-ZQi6fFTMBkfwwSPAlcGzArmNILz33QH99CL8jDfVWrzwVVcZc56Mge10jGk0zdRgWPXyL1/OXKjfw4vT5VtRQg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", - "@smithy/types": "^4.5.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5675,9 +6665,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", - "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5688,13 +6678,13 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", - "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.2.tgz", + "integrity": "sha512-wL9tZwWKy0x0qf6ffN7tX5CT03hb1e7XpjdepaKfKcPcyn5+jHAWPqivhF1Sw/T5DYi9wGcxsX8Lu07MOp2Puw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5702,14 +6692,14 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", - "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.2.tgz", + "integrity": "sha512-TlbnWAOoCuG2PgY0Hi3BGU1w2IXs3xDsD4E8WDfKRZUn2qx3wRA9mbYnmpWHPswTJCz2L+ebh+9OvD42sV4mNw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/service-error-classification": "^4.2.2", + "@smithy/types": "^4.7.1", "tslib": "^2.6.2" }, "engines": { @@ -5717,19 +6707,19 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", - "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.2.tgz", + "integrity": "sha512-RWYVuQVKtNbr7E0IxV8XHDId714yHPTxU6dHScd6wSMWAXboErzTG7+xqcL+K3r0Xg0cZSlfuNhl1J0rzMLSSw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-hex-encoding": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/types": "^4.7.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5737,9 +6727,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", - "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5750,19 +6740,42 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", - "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -5798,54 +6811,54 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", - "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", - "jiti": "^2.5.1", + "jiti": "^2.6.0", "lightningcss": "1.30.1", - "magic-string": "^0.30.18", + "magic-string": "^0.30.19", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.14" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", - "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.4", - "tar": "^7.4.3" + "tar": "^7.5.1" }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-x64": "4.1.13", - "@tailwindcss/oxide-freebsd-x64": "4.1.13", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-x64-musl": "4.1.13", - "@tailwindcss/oxide-wasm32-wasi": "4.1.13", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", - "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", "cpu": [ "arm64" ], @@ -5860,9 +6873,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", - "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", "cpu": [ "arm64" ], @@ -5877,9 +6890,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", - "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", "cpu": [ "x64" ], @@ -5894,9 +6907,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", - "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", "cpu": [ "x64" ], @@ -5911,9 +6924,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", - "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", "cpu": [ "arm" ], @@ -5928,9 +6941,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", - "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", "cpu": [ "arm64" ], @@ -5945,9 +6958,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", - "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", "cpu": [ "arm64" ], @@ -5962,9 +6975,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", - "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", "cpu": [ "x64" ], @@ -5979,9 +6992,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", - "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", "cpu": [ "x64" ], @@ -5996,9 +7009,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", - "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -6014,81 +7027,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.5", - "@emnapi/runtime": "^1.4.5", - "@emnapi/wasi-threads": "^1.0.4", - "@napi-rs/wasm-runtime": "^0.2.12", - "@tybys/wasm-util": "^0.10.0", - "tslib": "^2.8.0" + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", - "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", "cpu": [ "arm64" ], @@ -6103,9 +7056,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", - "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", "cpu": [ "x64" ], @@ -6120,17 +7073,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", - "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.13", - "@tailwindcss/oxide": "4.1.13", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", "postcss": "^8.4.41", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.14" } }, "node_modules/@tanstack/react-table": { @@ -6167,9 +7120,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "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": { @@ -6234,6 +7187,28 @@ "dev": true, "license": "MIT" }, + "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", @@ -6253,9 +7228,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -6334,19 +7309,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", - "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz", + "integrity": "sha512-Zo6uOA9157WRgBk/ZhMpTQ/iCWLMk7OIs/Q9jvHarMvrzUUP/MDdPHL2U1zpf57HrrWGv4nYQn5uIxna0xY3xw==", "dev": true, "license": "MIT", "dependencies": { @@ -6354,6 +7329,13 @@ "@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/pg": { "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", @@ -6366,6 +7348,13 @@ "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", @@ -6381,9 +7370,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", - "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6391,13 +7380,13 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/semver": { @@ -6408,6 +7397,28 @@ "license": "MIT" }, "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "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.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", @@ -6418,18 +7429,6 @@ "@types/node": "*" } }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, "node_modules/@types/swagger-ui-express": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", @@ -6447,12 +7446,17 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "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" + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } }, "node_modules/@types/ws": { "version": "8.18.1", @@ -6482,16 +7486,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", - "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/type-utils": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -6505,7 +7509,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.44.0", + "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -6520,15 +7524,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", - "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "engines": { @@ -6544,13 +7548,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", - "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.44.0", - "@typescript-eslint/types": "^8.44.0", + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "engines": { @@ -6565,13 +7569,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", - "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0" + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6582,9 +7586,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", - "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6598,14 +7602,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", - "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6622,9 +7626,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", - "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6635,15 +7639,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", - "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.44.0", - "@typescript-eslint/tsconfig-utils": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6715,15 +7719,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", - "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0" + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6738,12 +7742,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", - "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -7004,6 +8008,181 @@ "win32" ] }, + "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, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "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, + "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" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "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, + "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" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -7029,6 +8208,19 @@ "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", @@ -7054,10 +8246,52 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/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/ajv-formats/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/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -7081,6 +8315,13 @@ "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", @@ -7119,6 +8360,13 @@ "@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", @@ -7184,18 +8432,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-move": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/array-move/-/array-move-3.0.1.tgz", - "integrity": "sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -7361,6 +8597,44 @@ "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==", + "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" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -7377,9 +8651,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -7441,6 +8715,16 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "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", @@ -7505,6 +8789,13 @@ "node": ">=18" } }, + "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.12.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", @@ -7534,6 +8825,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -7636,10 +8961,20 @@ "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.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "funding": [ { "type": "opencollective", @@ -7708,6 +9043,16 @@ "node": ">=18" } }, + "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", @@ -7731,19 +9076,16 @@ } }, "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "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": "^5.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/cli-spinners": { @@ -7780,9 +9122,9 @@ } }, "node_modules/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -7792,9 +9134,9 @@ } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { @@ -7815,9 +9157,9 @@ } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -7866,17 +9208,16 @@ } }, "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", "license": "MIT", - "optional": true, "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "color-convert": "^3.0.1", + "color-string": "^2.0.0" }, "engines": { - "node": ">=12.5.0" + "node": ">=18" } }, "node_modules/color-convert": { @@ -7898,50 +9239,47 @@ "license": "MIT" }, "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", "license": "MIT", "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "node_modules/color-string/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", "license": "MIT", "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" } }, - "node_modules/colorspace/node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "node_modules/color/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" + "engines": { + "node": ">=12.20" } }, - "node_modules/colorspace/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/colorspace/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -8008,6 +9346,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -8071,35 +9416,6 @@ "node": ">= 0.10" } }, - "node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8141,6 +9457,49 @@ "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.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -8228,9 +9587,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8289,6 +9648,29 @@ "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", @@ -8342,9 +9724,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8356,6 +9738,13 @@ "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", @@ -8369,6 +9758,13 @@ "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", @@ -8437,9 +9833,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -8450,9 +9846,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz", - "integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==", + "version": "0.31.5", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.5.tgz", + "integrity": "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==", "dev": true, "license": "MIT", "dependencies": { @@ -8466,9 +9862,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.5", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", - "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", + "version": "0.44.6", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.6.tgz", + "integrity": "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -8620,15 +10016,15 @@ } }, "node_modules/eciesjs": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.15.tgz", - "integrity": "sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", + "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==", "dev": true, "license": "MIT", "dependencies": { - "@ecies/ciphers": "^0.2.3", + "@ecies/ciphers": "^0.2.4", "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.1", + "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" }, "engines": { @@ -8643,6 +10039,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -8694,6 +10097,60 @@ "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", @@ -8940,6 +10397,13 @@ "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", @@ -9095,19 +10559,19 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -9155,12 +10619,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz", - "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.4.tgz", + "integrity": "sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.3", + "@next/eslint-plugin-next": "15.5.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -9507,6 +10971,16 @@ "node": ">= 0.6" } }, + "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", @@ -9671,6 +11145,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", @@ -9700,10 +11191,13 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -9960,6 +11454,48 @@ "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", @@ -10035,6 +11571,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -10045,9 +11600,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "license": "MIT", "engines": { "node": ">=18" @@ -10133,9 +11688,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", + "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -10185,6 +11740,13 @@ "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.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", @@ -10201,15 +11763,13 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/globalthis": { @@ -10361,6 +11921,16 @@ "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", @@ -10551,14 +12121,14 @@ } }, "node_modules/intl-messageformat": { - "version": "10.7.16", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", - "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.2", + "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, @@ -10597,12 +12167,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -10769,13 +12333,14 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -10799,16 +12364,13 @@ } }, "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "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": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-map": { @@ -10972,13 +12534,13 @@ } }, "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "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": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11075,10 +12637,41 @@ "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.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", "bin": { @@ -11131,6 +12724,13 @@ "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", @@ -11144,15 +12744,16 @@ "license": "MIT" }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsonwebtoken": { @@ -11529,6 +13130,37 @@ "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==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11593,17 +13225,17 @@ "license": "MIT" }, "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "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": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11639,18 +13271,19 @@ } }, "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", - "engines": { - "node": "20 || >=22" + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/lucide-react": { - "version": "0.544.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", - "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -11687,6 +13320,20 @@ "node": ">= 0.4" } }, + "node_modules/maxmind": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.0.tgz", + "integrity": "sha512-ndhnbeQWKuiBU17BJ6cybUnvcyvNXaK+1VM5n9/I7+TIqAYFLDvX1DSoVfE1hgvZfudvAU9Ts1CW5sxYq/M8dA==", + "license": "MIT", + "dependencies": { + "mmdb-lib": "3.0.1", + "tiny-lru": "11.3.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/md-to-react-email": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz", @@ -11880,9 +13527,9 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -11892,28 +13539,33 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mmdb-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.1.tgz", + "integrity": "sha512-dyAyMR+cRykZd1mw5altC9f4vKpCsuywPwo8l/L5fKqDay2zmqT0mF/BvUoXnQiqGn+nceO914rkPKJoyFnGxA==", + "license": "MIT", + "engines": { + "node": ">=10", + "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", @@ -11923,6 +13575,23 @@ "node": "*" } }, + "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", @@ -11943,6 +13612,18 @@ "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", @@ -11968,9 +13649,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" @@ -11997,13 +13678,20 @@ "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.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", + "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -12016,14 +13704,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.4", + "@next/swc-darwin-x64": "15.5.4", + "@next/swc-linux-arm64-gnu": "15.5.4", + "@next/swc-linux-arm64-musl": "15.5.4", + "@next/swc-linux-x64-gnu": "15.5.4", + "@next/swc-linux-x64-musl": "15.5.4", + "@next/swc-win32-arm64-msvc": "15.5.4", + "@next/swc-win32-x64-msvc": "15.5.4", "sharp": "^0.34.3" }, "peerDependencies": { @@ -12050,9 +13738,9 @@ } }, "node_modules/next-intl": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz", - "integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.12.tgz", + "integrity": "sha512-yAmrQ3yx0zpNva/knniDvam3jT2d01Lv2aRgRxUIDL9zm9O4AsDjWbDIxX13t5RNf0KVnKkxH+iRcqEAmWecPg==", "funding": [ { "type": "individual", @@ -12063,7 +13751,7 @@ "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", - "use-intl": "^4.3.9" + "use-intl": "^4.3.12" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", @@ -12115,9 +13803,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -12176,10 +13864,28 @@ "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.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, "node_modules/nodemailer": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", - "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -12195,10 +13901,20 @@ "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.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.0.tgz", - "integrity": "sha512-d/P7DbvYgYNde9Ehfeq99+13/E7E82PfZPw8uYZASr9sQ3ZhBBCA9cXSJRA1COfJ6jDLJ0K36UJnXQWhCvLXuQ==", + "version": "11.6.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.2.tgz", + "integrity": "sha512-7iKzNfy8lWYs3zq4oFPa8EXZz5xt9gQNKJZau3B1ErLBb6bF7sBJ00x09485DOvRT2l5Gerbl3VlZNT57MxJVA==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -12242,7 +13958,6 @@ "ms", "node-gyp", "nopt", - "normalize-package-data", "npm-audit-report", "npm-install-checks", "npm-package-arg", @@ -12277,68 +13992,67 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.4", - "@npmcli/config": "^10.4.0", + "@npmcli/arborist": "^9.1.6", + "@npmcli/config": "^10.4.2", "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.2", - "@npmcli/package-json": "^6.2.0", - "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.1", + "@npmcli/promise-spawn": "^8.0.3", "@npmcli/redact": "^3.2.2", - "@npmcli/run-script": "^9.1.0", - "@sigstore/tuf": "^3.1.1", + "@npmcli/run-script": "^10.0.0", + "@sigstore/tuf": "^4.0.0", "abbrev": "^3.0.1", "archy": "~1.0.0", - "cacache": "^19.0.1", - "chalk": "^5.4.1", - "ci-info": "^4.3.0", + "cacache": "^20.0.1", + "chalk": "^5.6.2", + "ci-info": "^4.3.1", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^10.4.5", + "glob": "^11.0.3", "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.1.0", + "hosted-git-info": "^9.0.2", "ini": "^5.0.0", - "init-package-json": "^8.2.1", - "is-cidr": "^5.1.1", + "init-package-json": "^8.2.2", + "is-cidr": "^6.0.1", "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^10.0.1", - "libnpmdiff": "^8.0.7", - "libnpmexec": "^10.1.6", - "libnpmfund": "^7.0.7", - "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.7", - "libnpmpublish": "^11.1.0", - "libnpmsearch": "^9.0.0", - "libnpmteam": "^8.0.1", - "libnpmversion": "^8.0.1", - "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.5", + "libnpmaccess": "^10.0.3", + "libnpmdiff": "^8.0.9", + "libnpmexec": "^10.1.8", + "libnpmfund": "^7.0.9", + "libnpmorg": "^8.0.1", + "libnpmpack": "^9.0.9", + "libnpmpublish": "^11.1.2", + "libnpmsearch": "^9.0.1", + "libnpmteam": "^8.0.2", + "libnpmversion": "^8.0.2", + "make-fetch-happen": "^15.0.2", + "minimatch": "^10.0.3", "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^11.2.0", + "node-gyp": "^11.4.2", "nopt": "^8.1.0", - "normalize-package-data": "^7.0.1", "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.2", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.2", + "npm-install-checks": "^7.1.2", + "npm-package-arg": "^13.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-profile": "^12.0.0", + "npm-registry-fetch": "^19.0.0", "npm-user-validate": "^3.0.0", "p-map": "^7.0.3", - "pacote": "^21.0.0", + "pacote": "^21.0.3", "parse-conflict-json": "^4.0.0", "proc-log": "^5.0.0", "qrcode-terminal": "^0.12.0", "read": "^4.1.0", - "semver": "^7.7.2", + "semver": "^7.7.3", "spdx-expression-parse": "^4.0.0", "ssri": "^12.0.0", - "supports-color": "^10.0.0", - "tar": "^6.2.1", + "supports-color": "^10.2.2", + "tar": "^7.5.1", "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", + "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", "validate-npm-package-name": "^6.0.2", "which": "^5.0.0" @@ -12364,6 +14078,25 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", "inBundle": true, @@ -12381,7 +14114,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", + "version": "6.2.2", "inBundle": true, "license": "MIT", "engines": { @@ -12413,7 +14146,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", + "version": "7.1.2", "inBundle": true, "license": "MIT", "dependencies": { @@ -12443,55 +14176,54 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", + "version": "4.0.0", "inBundle": true, "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", + "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.4", + "version": "9.1.6", "inBundle": true, "license": "ISC", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^4.0.0", "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^9.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^3.0.0", "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", + "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^4.0.0", "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", + "@npmcli/run-script": "^10.0.0", "bin-links": "^5.0.0", - "cacache": "^19.0.1", + "cacache": "^20.0.1", "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", + "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", - "lru-cache": "^10.2.2", - "minimatch": "^9.0.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", "nopt": "^8.0.0", "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^21.0.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", "parse-conflict-json": "^4.0.0", "proc-log": "^5.0.0", "proggy": "^3.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", - "read-package-json-fast": "^4.0.0", "semver": "^7.3.7", "ssri": "^12.0.0", "treeverse": "^3.0.0", @@ -12505,12 +14237,12 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.0", + "version": "10.4.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^5.0.0", "nopt": "^8.1.0", @@ -12534,21 +14266,21 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.3", + "version": "7.0.0", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^8.0.0", "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { @@ -12567,25 +14299,25 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.2", + "version": "5.0.0", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0" + "@npmcli/package-json": "^7.0.0", + "glob": "^11.0.3", + "minimatch": "^10.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.1", + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "cacache": "^19.0.0", + "cacache": "^20.0.0", "json-parse-even-better-errors": "^4.0.0", "pacote": "^21.0.0", "proc-log": "^5.0.0", @@ -12612,24 +14344,24 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.2.0", + "version": "7.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", + "@npmcli/git": "^7.0.0", + "glob": "^11.0.3", + "hosted-git-info": "^9.0.0", "json-parse-even-better-errors": "^4.0.0", "proc-log": "^5.0.0", "semver": "^7.5.3", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", + "version": "8.0.3", "inBundle": true, "license": "ISC", "dependencies": { @@ -12659,19 +14391,19 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.1.0", + "version": "10.0.0", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", + "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^8.0.0", "node-gyp": "^11.0.0", "proc-log": "^5.0.0", "which": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@pkgjs/parseargs": { @@ -12684,26 +14416,26 @@ } }, "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "3.1.0", + "version": "4.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "2.0.0", + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.4.3", + "version": "0.5.0", "inBundle": true, "license": "Apache-2.0", "engines": { @@ -12711,44 +14443,44 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "3.1.0", + "version": "4.0.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.2", "proc-log": "^5.0.0", "promise-retry": "^2.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "3.1.1", + "version": "4.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.1", - "tuf-js": "^3.0.1" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "2.1.1", + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@tufjs/canonical-json": { @@ -12760,7 +14492,7 @@ } }, "node_modules/npm/node_modules/@tufjs/models": { - "version": "3.0.1", + "version": "4.0.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -12768,7 +14500,21 @@ "minimatch": "^9.0.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm/node_modules/abbrev": { @@ -12796,7 +14542,7 @@ } }, "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.1", + "version": "6.2.3", "inBundle": true, "license": "MIT", "engines": { @@ -12856,86 +14602,28 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", + "version": "20.0.1", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", - "tar": "^7.4.3", "unique-filename": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/chalk": { - "version": "5.4.1", + "version": "5.6.2", "inBundle": true, "license": "MIT", "engines": { @@ -12946,15 +14634,15 @@ } }, "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", + "version": "3.0.0", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.3.0", + "version": "4.3.1", "funding": [ { "type": "github", @@ -12968,14 +14656,14 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "4.1.3", + "version": "5.0.1", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "ip-regex": "^5.0.0" + "ip-regex": "5.0.0" }, "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/npm/node_modules/cli-columns": { @@ -13032,6 +14720,11 @@ "node": ">= 8" } }, + "node_modules/npm/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", "inBundle": true, @@ -13058,7 +14751,7 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.4.1", + "version": "4.4.3", "inBundle": true, "license": "MIT", "dependencies": { @@ -13074,7 +14767,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "7.0.0", + "version": "8.0.2", "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -13153,20 +14846,23 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.5", + "version": "11.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -13177,14 +14873,14 @@ "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.1.0", + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/http-cache-semantics": { @@ -13229,14 +14925,14 @@ } }, "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", + "version": "8.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "minimatch": "^9.0.0" + "minimatch": "^10.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/imurmurhash": { @@ -13256,30 +14952,26 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "8.2.1", + "version": "8.2.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/package-json": "^6.1.0", - "npm-package-arg": "^12.0.0", + "@npmcli/package-json": "^7.0.0", + "npm-package-arg": "^13.0.0", "promzard": "^2.0.0", "read": "^4.0.0", - "semver": "^7.3.5", + "semver": "^7.7.2", "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^6.0.2" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/ip-address": { - "version": "9.0.5", + "version": "10.0.1", "inBundle": true, "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -13296,14 +14988,14 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "5.1.1", + "version": "6.0.1", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "^4.1.1" + "cidr-regex": "5.0.1" }, "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/npm/node_modules/is-fullwidth-code-point": { @@ -13315,29 +15007,27 @@ } }, "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", + "version": "3.1.1", "inBundle": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=16" + } }, "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.3", + "version": "4.1.1", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/npm/node_modules/jsbn": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "4.0.0", "inBundle": true, @@ -13373,50 +15063,51 @@ "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.1", + "version": "10.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.7", + "version": "8.0.9", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.4", + "@npmcli/arborist": "^9.1.6", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", - "diff": "^7.0.0", - "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", - "tar": "^6.2.1" + "diff": "^8.0.2", + "minimatch": "^10.0.3", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "tar": "^7.5.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.6", + "version": "10.1.8", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.4", - "@npmcli/package-json": "^6.1.1", - "@npmcli/run-script": "^9.0.1", + "@npmcli/arborist": "^9.1.6", + "@npmcli/package-json": "^7.0.0", + "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", "semver": "^7.3.7", + "signal-exit": "^4.1.0", "walk-up-path": "^4.0.0" }, "engines": { @@ -13424,54 +15115,54 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.7", + "version": "7.0.9", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.4" + "@npmcli/arborist": "^9.1.6" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.0", + "version": "8.0.1", "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^19.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.7", + "version": "9.0.9", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.4", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0" + "@npmcli/arborist": "^9.1.6", + "@npmcli/run-script": "^10.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.1.0", + "version": "11.1.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/package-json": "^6.2.0", + "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0", "proc-log": "^5.0.0", "semver": "^7.3.7", - "sigstore": "^3.0.0", + "sigstore": "^4.0.0", "ssri": "^12.0.0" }, "engines": { @@ -13479,35 +15170,35 @@ } }, "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.0", + "version": "9.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^19.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.1", + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^19.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.1", + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", + "@npmcli/git": "^7.0.0", + "@npmcli/run-script": "^10.0.0", "json-parse-even-better-errors": "^4.0.0", "proc-log": "^5.0.0", "semver": "^7.3.7" @@ -13517,17 +15208,20 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "10.4.3", + "version": "11.2.2", "inBundle": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.3", + "version": "15.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", @@ -13539,26 +15233,18 @@ "ssri": "^12.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/minimatch": { - "version": "9.0.5", + "version": "10.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -13599,17 +15285,6 @@ "encoding": "^0.1.13" } }, - "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", "inBundle": true, @@ -13677,37 +15352,14 @@ } }, "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", + "version": "3.1.0", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/npm/node_modules/ms": { @@ -13723,8 +15375,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm/node_modules/node-gyp": { - "version": "11.2.0", + "version": "11.4.2", "inBundle": true, "license": "MIT", "dependencies": { @@ -13746,61 +15406,129 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { "version": "3.0.0", "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.2", - "inBundle": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "minipass": "^7.1.2" + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": ">= 18" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "19.0.1", "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", + "node_modules/npm/node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "14.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" }, "engines": { - "node": ">=18" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/path-scurry": { + "version": "1.11.1", "inBundle": true, "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm/node_modules/nopt": { @@ -13817,19 +15545,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^8.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/npm-audit-report": { "version": "6.0.0", "inBundle": true, @@ -13850,7 +15565,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.1", + "version": "7.1.2", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -13869,83 +15584,73 @@ } }, "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.2", + "version": "13.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "hosted-git-info": "^8.0.0", + "hosted-git-info": "^9.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.0", + "version": "10.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "ignore-walk": "^7.0.0" + "ignore-walk": "^8.0.0", + "proc-log": "^5.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", + "version": "11.0.1", "inBundle": true, "license": "ISC", "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", + "version": "12.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^18.0.0", + "npm-registry-fetch": "^19.0.0", "proc-log": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.2", + "version": "19.0.0", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/redact": "^3.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", + "npm-package-arg": "^13.0.0", "proc-log": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-user-validate": { @@ -13973,27 +15678,27 @@ "license": "BlueOak-1.0.0" }, "node_modules/npm/node_modules/pacote": { - "version": "21.0.0", + "version": "21.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", + "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", + "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^10.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", + "sigstore": "^4.0.0", "ssri": "^12.0.0", - "tar": "^6.1.11" + "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" @@ -14024,15 +15729,15 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "1.11.1", + "version": "2.0.0", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -14131,18 +15836,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/retry": { "version": "0.12.0", "inBundle": true, @@ -14158,7 +15851,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.7.2", + "version": "7.7.3", "inBundle": true, "license": "ISC", "bin": { @@ -14199,19 +15892,19 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "3.1.0", + "version": "4.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "@sigstore/sign": "^3.1.0", - "@sigstore/tuf": "^3.1.0", - "@sigstore/verify": "^2.1.0" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/smart-buffer": { @@ -14224,11 +15917,11 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.6", + "version": "2.8.7", "inBundle": true, "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -14282,15 +15975,10 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.21", + "version": "3.0.22", "inBundle": true, "license": "CC0-1.0" }, - "node_modules/npm/node_modules/sprintf-js": { - "version": "1.1.3", - "inBundle": true, - "license": "BSD-3-Clause" - }, "node_modules/npm/node_modules/ssri": { "version": "12.0.0", "inBundle": true, @@ -14353,7 +16041,7 @@ } }, "node_modules/npm/node_modules/supports-color": { - "version": "10.0.0", + "version": "10.2.2", "inBundle": true, "license": "MIT", "engines": { @@ -14364,49 +16052,26 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "6.2.1", + "version": "7.5.1", "inBundle": true, "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minipass": { + "node_modules/npm/node_modules/tar/node_modules/yallist": { "version": "5.0.0", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/npm/node_modules/text-table": { @@ -14415,17 +16080,17 @@ "license": "MIT" }, "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", + "version": "2.0.2", "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.14", + "version": "0.2.15", "inBundle": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -14435,9 +16100,12 @@ } }, "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", + "version": "6.5.0", "inBundle": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -14467,16 +16135,16 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "3.1.0", + "version": "4.0.0", "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "3.0.1", + "@tufjs/models": "4.0.0", "debug": "^4.4.1", - "make-fetch-happen": "^14.0.3" + "make-fetch-happen": "^15.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/unique-filename": { @@ -14554,14 +16222,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", "inBundle": true, @@ -14610,7 +16270,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", + "version": "6.2.2", "inBundle": true, "license": "MIT", "engines": { @@ -14642,7 +16302,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", + "version": "7.1.2", "inBundle": true, "license": "MIT", "dependencies": { @@ -14672,6 +16332,19 @@ "inBundle": true, "license": "ISC" }, + "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", @@ -14908,95 +16581,50 @@ } }, "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "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": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "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": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "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": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "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": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/oslo": { @@ -15419,13 +17047,23 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", "engines": { - "node": ">=16" + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { @@ -15561,10 +17199,30 @@ "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.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", - "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "dev": true, "license": "MIT", "dependencies": { @@ -15624,6 +17282,97 @@ "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", @@ -15713,6 +17462,33 @@ "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", @@ -15857,6 +17633,16 @@ ], "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", @@ -15867,18 +17653,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/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==", + "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/rc": { @@ -15906,53 +17708,46 @@ } }, "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.1" + "react": "^19.2.0" } }, "node_modules/react-easy-sort": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.7.0.tgz", - "integrity": "sha512-82I63kXdawFhhlFrWPrI74DL48v2LKs7e7PLf5le2E/eIR9+XyCEdL4Pyjbru8XjvtQ60mPLb6oextc4PPR8Lg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.8.0.tgz", + "integrity": "sha512-6CUvG0rPyO8H9MTel38r/gmPemIKcOSkvgZQtrxILYFPfGZnmkLVU3YSVHEg22D+pJMoeVRdJpuF2kD2dqeIEw==", "license": "MIT", "dependencies": { - "array-move": "^3.0.1", - "tslib": "2.0.1" + "tslib": "^2.8.1" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, - "node_modules/react-easy-sort/node_modules/tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", - "license": "0BSD" - }, "node_modules/react-email": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.2.11.tgz", - "integrity": "sha512-/7TXRgsTrXcV1u7kc5ZXDVlPvZqEBaYcflMhE2FgWIJh3OHLjj2FqctFTgYcp0iwzbR59a7gzJLmSKyD0wYJEQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", + "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", "dev": true, "license": "MIT", "dependencies": { @@ -15980,6 +17775,35 @@ "node": ">=18.0.0" } }, + "node_modules/react-email/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-email/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-email/node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -15990,6 +17814,39 @@ "node": ">=18" } }, + "node_modules/react-email/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-email/node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -16000,17 +17857,139 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/react-email/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/react-email/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/signal-exit": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-email/node_modules/tsconfig-paths": { @@ -16029,9 +18008,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.62.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", - "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "version": "7.65.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", + "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -16143,6 +18122,16 @@ } } }, + "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", @@ -16185,6 +18174,12 @@ "node": ">=0.8.8" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16227,6 +18222,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resend": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.1.3.tgz", + "integrity": "sha512-vHRdmU3q+nS5x7cYHZpAQ5zpZE+DV+7q6axIUiRcxYsoUpjBuW50zwdrOz+8O6vUbjGFIz4r2qkt4s+2G0y4GA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -16266,49 +18288,17 @@ } }, "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "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": "^7.0.0", - "signal-exit": "^4.1.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, "node_modules/reusify": { @@ -16448,9 +18438,66 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "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": { @@ -16466,9 +18513,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16499,6 +18546,16 @@ "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", @@ -16567,15 +18624,15 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "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": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "engines": { @@ -16585,28 +18642,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/shebang-command": { @@ -16754,15 +18811,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -16850,6 +18898,40 @@ } } }, + "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", @@ -16947,6 +19029,17 @@ "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", @@ -16977,6 +19070,12 @@ "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", @@ -17001,6 +19100,19 @@ "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/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -17212,9 +19324,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17315,6 +19427,126 @@ } } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "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/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/sucrase/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17340,9 +19572,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", - "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", + "version": "5.29.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", + "integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -17374,15 +19606,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", - "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -17394,17 +19626,16 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { @@ -17412,9 +19643,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -17445,12 +19676,115 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "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-lru": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.3.4.tgz", + "integrity": "sha512-UxWEfRKpFCabAf6fkTNdlfSw/RDUJ/4C6i1aLZaDnGF82PERHyYhz5CMCVYXtLt34LbqgfpJ2bjmgGKgxuF/6A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -17459,13 +19793,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -17495,12 +19829,6 @@ "node": ">=0.6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -17522,6 +19850,13 @@ "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", @@ -17630,6 +19965,18 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -17646,9 +19993,9 @@ } }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", "dependencies": { @@ -17665,6 +20012,24 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -17678,9 +20043,9 @@ } }, "node_modules/tw-animate-css": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", - "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -17698,6 +20063,16 @@ "node": ">= 0.8.0" } }, + "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==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -17787,9 +20162,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17800,16 +20175,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", - "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", + "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.44.0", - "@typescript-eslint/parser": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0" + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17842,9 +20217,9 @@ } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "devOptional": true, "license": "MIT" }, @@ -17891,6 +20266,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -17921,10 +20327,23 @@ } } }, + "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.3.9", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz", - "integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.12.tgz", + "integrity": "sha512-RxW2/D17irlDOJOzClKl+kWA7ReGLpo/A8f/LF7w1kIxO6mPKVh422JJ/pDCcvtYFCI4aPtn1AXUfELKbM+7tg==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -17958,9 +20377,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -18007,6 +20426,30 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "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", @@ -18016,20 +20459,110 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, "license": "MIT", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "@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.2.0", + "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" } }, "node_modules/which": { @@ -18134,13 +20667,13 @@ } }, "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -18282,9 +20815,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -18320,6 +20853,15 @@ } } }, + "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", @@ -18339,14 +20881,11 @@ } }, "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "license": "ISC" }, "node_modules/yaml": { "version": "2.8.1", @@ -18387,9 +20926,9 @@ } }, "node_modules/yargs/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { @@ -18422,9 +20961,9 @@ } }, "node_modules/yoctocolors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", - "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 342e28a7..4530e169 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:clear-migrations": "rm -rf server/migrations", + "set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts", + "set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts", + "next:build": "next build", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", @@ -49,11 +52,11 @@ "@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.3", - "@react-email/render": "^1.2.0", + "@react-email/components": "0.5.6", + "@react-email/render": "^1.3.2", "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.1.2", - "@simplewebauthn/server": "^9.0.3", + "@simplewebauthn/browser": "^13.2.2", + "@simplewebauthn/server": "^13.2.2", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -68,9 +71,9 @@ "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "drizzle-orm": "0.44.5", - "eslint": "9.35.0", - "eslint-config-next": "15.5.3", + "drizzle-orm": "0.44.6", + "eslint": "9.37.0", + "eslint-config-next": "15.5.4", "express": "5.1.0", "express-rate-limit": "8.1.0", "glob": "11.0.3", @@ -81,31 +84,33 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", + "maxmind": "5.0.0", "moment": "2.30.1", - "next": "15.5.3", - "next-intl": "^4.3.9", + "next": "15.5.4", + "next-intl": "^4.3.12", "next-themes": "0.4.6", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.6", - "npm": "^11.6.0", + "nodemailer": "7.0.9", + "npm": "^11.6.2", "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "react": "19.1.1", - "react-dom": "19.1.1", - "react-easy-sort": "^1.7.0", - "react-hook-form": "7.62.0", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-easy-sort": "^1.8.0", + "react-hook-form": "7.65.0", "react-icons": "^5.5.0", "rebuild": "0.1.2", - "semver": "^7.7.2", + "resend": "^6.1.2", + "semver": "^7.7.3", "swagger-ui-express": "^5.0.1", "tailwind-merge": "3.3.1", "tw-animate-css": "^1.3.8", "uuid": "^13.0.0", "vaul": "1.1.2", - "winston": "3.17.0", + "winston": "3.18.3", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", "yargs": "18.0.0", @@ -113,9 +118,10 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.49.1", + "@dotenvx/dotenvx": "1.51.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "^4.1.13", + "@react-email/preview-server": "4.3.0", + "@tailwindcss/postcss": "^4.1.14", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", @@ -125,25 +131,25 @@ "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.5.2", - "@types/nodemailer": "7.0.1", + "@types/node": "24.7.2", + "@types/nodemailer": "7.0.2", "@types/pg": "8.15.5", - "@types/react": "19.1.13", - "@types/react-dom": "19.1.9", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.1", "@types/semver": "^7.7.1", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.4", + "drizzle-kit": "0.31.5", "esbuild": "0.25.10", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.2.11", + "react-email": "4.3.0", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", - "tsx": "4.20.5", + "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.44.0" + "typescript-eslint": "^8.46.0" }, "overrides": { "emblor": { diff --git a/public/logo/word_mark_black.png b/public/logo/word_mark_black.png index ba6fb84e..cc412165 100644 Binary files a/public/logo/word_mark_black.png and b/public/logo/word_mark_black.png differ diff --git a/public/logo/word_mark_white.png b/public/logo/word_mark_white.png index fb7a252d..cd02b58a 100644 Binary files a/public/logo/word_mark_white.png and b/public/logo/word_mark_white.png differ diff --git a/server/apiServer.ts b/server/apiServer.ts index a400555b..8f1c1600 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -16,7 +16,8 @@ import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import createHttpError from "http-errors"; import HttpCode from "./types/HttpCode"; import requestTimeoutMiddleware from "./middlewares/requestTimeout"; -import { createStore } from "./lib/rateLimitStore"; +import { createStore } from "@server/lib/rateLimitStore"; +import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; const dev = config.isDev; const externalPort = config.getRawConfig().server.external_port; @@ -31,7 +32,6 @@ export function createApiServer() { } const corsConfig = config.getRawConfig().server.cors; - const options = { ...(corsConfig?.origins ? { origin: corsConfig.origins } @@ -48,7 +48,6 @@ export function createApiServer() { }; logger.debug("Using CORS options", options); - apiServer.use(cors(options)); if (!dev) { @@ -56,6 +55,7 @@ export function createApiServer() { apiServer.use(csrfProtectionMiddleware); } + apiServer.use(stripDuplicateSesions); apiServer.use(cookieParser()); apiServer.use(express.json()); @@ -70,7 +70,8 @@ export function createApiServer() { 60 * 1000, max: config.getRawConfig().rate_limits.global.max_requests, - keyGenerator: (req) => `apiServerGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`, + keyGenerator: (req) => + `apiServerGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`, handler: (req, res, next) => { const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`; return next( diff --git a/server/auth/actions.ts b/server/auth/actions.ts index f020c2ff..8740c865 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -61,6 +61,7 @@ export enum ActionsEnum { getUser = "getUser", setResourcePassword = "setResourcePassword", setResourcePincode = "setResourcePincode", + setResourceHeaderAuth = "setResourceHeaderAuth", setResourceWhitelist = "setResourceWhitelist", getResourceWhitelist = "getResourceWhitelist", generateAccessToken = "generateAccessToken", @@ -99,10 +100,23 @@ export enum ActionsEnum { listApiKeyActions = "listApiKeyActions", listApiKeys = "listApiKeys", getApiKey = "getApiKey", + getCertificate = "getCertificate", + restartCertificate = "restartCertificate", + billing = "billing", createOrgDomain = "createOrgDomain", deleteOrgDomain = "deleteOrgDomain", restartOrgDomain = "restartOrgDomain", + sendUsageNotification = "sendUsageNotification", + createRemoteExitNode = "createRemoteExitNode", + updateRemoteExitNode = "updateRemoteExitNode", + getRemoteExitNode = "getRemoteExitNode", + listRemoteExitNode = "listRemoteExitNode", + deleteRemoteExitNode = "deleteRemoteExitNode", updateOrgUser = "updateOrgUser", + createLoginPage = "createLoginPage", + updateLoginPage = "updateLoginPage", + getLoginPage = "getLoginPage", + deleteLoginPage = "deleteLoginPage", applyBlueprint = "applyBlueprint" } @@ -180,7 +194,6 @@ export async function checkUserActionPermission( return roleActionPermission.length > 0; - return false; } catch (error) { console.error("Error checking user action permission:", error); throw createHttpError( diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index 514bee00..e846396d 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -3,13 +3,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { - resourceSessions, - Session, - sessions, - User, - users -} from "@server/db"; +import { resourceSessions, Session, sessions, User, users } from "@server/db"; import { db } from "@server/db"; import { eq, inArray } from "drizzle-orm"; import config from "@server/lib/config"; @@ -24,8 +18,9 @@ export const SESSION_COOKIE_EXPIRES = 60 * 60 * config.getRawConfig().server.dashboard_session_length_hours; -export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ? - "." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined; +export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url + ? new URL(config.getRawConfig().app.dashboard_url!).hostname + : undefined; export function generateSessionToken(): string { const bytes = new Uint8Array(20); @@ -98,8 +93,8 @@ export async function invalidateSession(sessionId: string): Promise { try { await db.transaction(async (trx) => { await trx - .delete(resourceSessions) - .where(eq(resourceSessions.userSessionId, sessionId)); + .delete(resourceSessions) + .where(eq(resourceSessions.userSessionId, sessionId)); await trx.delete(sessions).where(eq(sessions.sessionId, sessionId)); }); } catch (e) { @@ -111,9 +106,9 @@ export async function invalidateAllSessions(userId: string): Promise { try { await db.transaction(async (trx) => { const userSessions = await trx - .select() - .from(sessions) - .where(eq(sessions.userId, userId)); + .select() + .from(sessions) + .where(eq(sessions.userId, userId)); await trx.delete(resourceSessions).where( inArray( resourceSessions.userSessionId, diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 511dadda..31ab2b38 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db"; import { db } from "@server/db"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; -import axios from "axios"; -import logger from "@server/logger"; -import { tokenManager } from "@server/lib/tokenManager"; export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; @@ -65,29 +62,6 @@ export async function validateResourceSessionToken( token: string, resourceId: number ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, { - token: token - }, await tokenManager.getAuthHeader()); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error validating resource session token in hybrid mode:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error validating resource session token in hybrid mode:", error); - } - return { resourceSession: null }; - } - } - const sessionId = encodeHexLowerCase( sha256(new TextEncoder().encode(token)) ); @@ -199,14 +173,14 @@ export function serializeResourceSessionCookie( const now = new Date().getTime(); if (!isHttp) { if (expiresAt === undefined) { - return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`; + return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${domain}`; } - return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`; + return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`; } else { if (expiresAt === undefined) { - return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`; + return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`; } - return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`; + return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`; } } @@ -216,9 +190,9 @@ export function createBlankResourceSessionTokenCookie( isHttp: boolean = false ): string { if (!isHttp) { - return `${cookieName}_s=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`; + return `${cookieName}_s=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${domain}`; } else { - return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${"." + domain}`; + return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${domain}`; } } diff --git a/server/build.ts b/server/build.ts deleted file mode 100644 index babe5e8b..00000000 --- a/server/build.ts +++ /dev/null @@ -1 +0,0 @@ -export const build = "oss" as any; diff --git a/server/cleanup.ts b/server/cleanup.ts new file mode 100644 index 00000000..de54ed77 --- /dev/null +++ b/server/cleanup.ts @@ -0,0 +1,13 @@ +import { cleanup as wsCleanup } from "@server/routers/ws"; + +async function cleanup() { + await wsCleanup(); + + process.exit(0); +} + +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 new file mode 100644 index 00000000..2907fd69 --- /dev/null +++ b/server/db/countries.ts @@ -0,0 +1,1014 @@ +export const COUNTRIES = [ + { + "name": "ALL COUNTRIES", + "code": "ALL" // THIS IS AN INVALID CC SO IT WILL NEVER MATCH + }, + { + "name": "Afghanistan", + "code": "AF" + }, + { + "name": "Albania", + "code": "AL" + }, + { + "name": "Algeria", + "code": "DZ" + }, + { + "name": "American Samoa", + "code": "AS" + }, + { + "name": "Andorra", + "code": "AD" + }, + { + "name": "Angola", + "code": "AO" + }, + { + "name": "Anguilla", + "code": "AI" + }, + { + "name": "Antarctica", + "code": "AQ" + }, + { + "name": "Antigua and Barbuda", + "code": "AG" + }, + { + "name": "Argentina", + "code": "AR" + }, + { + "name": "Armenia", + "code": "AM" + }, + { + "name": "Aruba", + "code": "AW" + }, + { + "name": "Asia/Pacific Region", + "code": "AP" + }, + { + "name": "Australia", + "code": "AU" + }, + { + "name": "Austria", + "code": "AT" + }, + { + "name": "Azerbaijan", + "code": "AZ" + }, + { + "name": "Bahamas", + "code": "BS" + }, + { + "name": "Bahrain", + "code": "BH" + }, + { + "name": "Bangladesh", + "code": "BD" + }, + { + "name": "Barbados", + "code": "BB" + }, + { + "name": "Belarus", + "code": "BY" + }, + { + "name": "Belgium", + "code": "BE" + }, + { + "name": "Belize", + "code": "BZ" + }, + { + "name": "Benin", + "code": "BJ" + }, + { + "name": "Bermuda", + "code": "BM" + }, + { + "name": "Bhutan", + "code": "BT" + }, + { + "name": "Bolivia", + "code": "BO" + }, + { + "name": "Bonaire, Sint Eustatius and Saba", + "code": "BQ" + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA" + }, + { + "name": "Botswana", + "code": "BW" + }, + { + "name": "Bouvet Island", + "code": "BV" + }, + { + "name": "Brazil", + "code": "BR" + }, + { + "name": "British Indian Ocean Territory", + "code": "IO" + }, + { + "name": "Brunei Darussalam", + "code": "BN" + }, + { + "name": "Bulgaria", + "code": "BG" + }, + { + "name": "Burkina Faso", + "code": "BF" + }, + { + "name": "Burundi", + "code": "BI" + }, + { + "name": "Cambodia", + "code": "KH" + }, + { + "name": "Cameroon", + "code": "CM" + }, + { + "name": "Canada", + "code": "CA" + }, + { + "name": "Cape Verde", + "code": "CV" + }, + { + "name": "Cayman Islands", + "code": "KY" + }, + { + "name": "Central African Republic", + "code": "CF" + }, + { + "name": "Chad", + "code": "TD" + }, + { + "name": "Chile", + "code": "CL" + }, + { + "name": "China", + "code": "CN" + }, + { + "name": "Christmas Island", + "code": "CX" + }, + { + "name": "Cocos (Keeling) Islands", + "code": "CC" + }, + { + "name": "Colombia", + "code": "CO" + }, + { + "name": "Comoros", + "code": "KM" + }, + { + "name": "Congo", + "code": "CG" + }, + { + "name": "Congo, The Democratic Republic of the", + "code": "CD" + }, + { + "name": "Cook Islands", + "code": "CK" + }, + { + "name": "Costa Rica", + "code": "CR" + }, + { + "name": "Croatia", + "code": "HR" + }, + { + "name": "Cuba", + "code": "CU" + }, + { + "name": "Curaçao", + "code": "CW" + }, + { + "name": "Cyprus", + "code": "CY" + }, + { + "name": "Czech Republic", + "code": "CZ" + }, + { + "name": "Côte d'Ivoire", + "code": "CI" + }, + { + "name": "Denmark", + "code": "DK" + }, + { + "name": "Djibouti", + "code": "DJ" + }, + { + "name": "Dominica", + "code": "DM" + }, + { + "name": "Dominican Republic", + "code": "DO" + }, + { + "name": "Ecuador", + "code": "EC" + }, + { + "name": "Egypt", + "code": "EG" + }, + { + "name": "El Salvador", + "code": "SV" + }, + { + "name": "Equatorial Guinea", + "code": "GQ" + }, + { + "name": "Eritrea", + "code": "ER" + }, + { + "name": "Estonia", + "code": "EE" + }, + { + "name": "Ethiopia", + "code": "ET" + }, + { + "name": "Falkland Islands (Malvinas)", + "code": "FK" + }, + { + "name": "Faroe Islands", + "code": "FO" + }, + { + "name": "Fiji", + "code": "FJ" + }, + { + "name": "Finland", + "code": "FI" + }, + { + "name": "France", + "code": "FR" + }, + { + "name": "French Guiana", + "code": "GF" + }, + { + "name": "French Polynesia", + "code": "PF" + }, + { + "name": "French Southern Territories", + "code": "TF" + }, + { + "name": "Gabon", + "code": "GA" + }, + { + "name": "Gambia", + "code": "GM" + }, + { + "name": "Georgia", + "code": "GE" + }, + { + "name": "Germany", + "code": "DE" + }, + { + "name": "Ghana", + "code": "GH" + }, + { + "name": "Gibraltar", + "code": "GI" + }, + { + "name": "Greece", + "code": "GR" + }, + { + "name": "Greenland", + "code": "GL" + }, + { + "name": "Grenada", + "code": "GD" + }, + { + "name": "Guadeloupe", + "code": "GP" + }, + { + "name": "Guam", + "code": "GU" + }, + { + "name": "Guatemala", + "code": "GT" + }, + { + "name": "Guernsey", + "code": "GG" + }, + { + "name": "Guinea", + "code": "GN" + }, + { + "name": "Guinea-Bissau", + "code": "GW" + }, + { + "name": "Guyana", + "code": "GY" + }, + { + "name": "Haiti", + "code": "HT" + }, + { + "name": "Heard Island and Mcdonald Islands", + "code": "HM" + }, + { + "name": "Holy See (Vatican City State)", + "code": "VA" + }, + { + "name": "Honduras", + "code": "HN" + }, + { + "name": "Hong Kong", + "code": "HK" + }, + { + "name": "Hungary", + "code": "HU" + }, + { + "name": "Iceland", + "code": "IS" + }, + { + "name": "India", + "code": "IN" + }, + { + "name": "Indonesia", + "code": "ID" + }, + { + "name": "Iran, Islamic Republic Of", + "code": "IR" + }, + { + "name": "Iraq", + "code": "IQ" + }, + { + "name": "Ireland", + "code": "IE" + }, + { + "name": "Isle of Man", + "code": "IM" + }, + { + "name": "Israel", + "code": "IL" + }, + { + "name": "Italy", + "code": "IT" + }, + { + "name": "Jamaica", + "code": "JM" + }, + { + "name": "Japan", + "code": "JP" + }, + { + "name": "Jersey", + "code": "JE" + }, + { + "name": "Jordan", + "code": "JO" + }, + { + "name": "Kazakhstan", + "code": "KZ" + }, + { + "name": "Kenya", + "code": "KE" + }, + { + "name": "Kiribati", + "code": "KI" + }, + { + "name": "Korea, Republic of", + "code": "KR" + }, + { + "name": "Kuwait", + "code": "KW" + }, + { + "name": "Kyrgyzstan", + "code": "KG" + }, + { + "name": "Laos", + "code": "LA" + }, + { + "name": "Latvia", + "code": "LV" + }, + { + "name": "Lebanon", + "code": "LB" + }, + { + "name": "Lesotho", + "code": "LS" + }, + { + "name": "Liberia", + "code": "LR" + }, + { + "name": "Libyan Arab Jamahiriya", + "code": "LY" + }, + { + "name": "Liechtenstein", + "code": "LI" + }, + { + "name": "Lithuania", + "code": "LT" + }, + { + "name": "Luxembourg", + "code": "LU" + }, + { + "name": "Macao", + "code": "MO" + }, + { + "name": "Madagascar", + "code": "MG" + }, + { + "name": "Malawi", + "code": "MW" + }, + { + "name": "Malaysia", + "code": "MY" + }, + { + "name": "Maldives", + "code": "MV" + }, + { + "name": "Mali", + "code": "ML" + }, + { + "name": "Malta", + "code": "MT" + }, + { + "name": "Marshall Islands", + "code": "MH" + }, + { + "name": "Martinique", + "code": "MQ" + }, + { + "name": "Mauritania", + "code": "MR" + }, + { + "name": "Mauritius", + "code": "MU" + }, + { + "name": "Mayotte", + "code": "YT" + }, + { + "name": "Mexico", + "code": "MX" + }, + { + "name": "Micronesia, Federated States of", + "code": "FM" + }, + { + "name": "Moldova, Republic of", + "code": "MD" + }, + { + "name": "Monaco", + "code": "MC" + }, + { + "name": "Mongolia", + "code": "MN" + }, + { + "name": "Montenegro", + "code": "ME" + }, + { + "name": "Montserrat", + "code": "MS" + }, + { + "name": "Morocco", + "code": "MA" + }, + { + "name": "Mozambique", + "code": "MZ" + }, + { + "name": "Myanmar", + "code": "MM" + }, + { + "name": "Namibia", + "code": "NA" + }, + { + "name": "Nauru", + "code": "NR" + }, + { + "name": "Nepal", + "code": "NP" + }, + { + "name": "Netherlands", + "code": "NL" + }, + { + "name": "Netherlands Antilles", + "code": "AN" + }, + { + "name": "New Caledonia", + "code": "NC" + }, + { + "name": "New Zealand", + "code": "NZ" + }, + { + "name": "Nicaragua", + "code": "NI" + }, + { + "name": "Niger", + "code": "NE" + }, + { + "name": "Nigeria", + "code": "NG" + }, + { + "name": "Niue", + "code": "NU" + }, + { + "name": "Norfolk Island", + "code": "NF" + }, + { + "name": "North Korea", + "code": "KP" + }, + { + "name": "North Macedonia", + "code": "MK" + }, + { + "name": "Northern Mariana Islands", + "code": "MP" + }, + { + "name": "Norway", + "code": "NO" + }, + { + "name": "Oman", + "code": "OM" + }, + { + "name": "Pakistan", + "code": "PK" + }, + { + "name": "Palau", + "code": "PW" + }, + { + "name": "Palestinian Territory, Occupied", + "code": "PS" + }, + { + "name": "Panama", + "code": "PA" + }, + { + "name": "Papua New Guinea", + "code": "PG" + }, + { + "name": "Paraguay", + "code": "PY" + }, + { + "name": "Peru", + "code": "PE" + }, + { + "name": "Philippines", + "code": "PH" + }, + { + "name": "Pitcairn Islands", + "code": "PN" + }, + { + "name": "Poland", + "code": "PL" + }, + { + "name": "Portugal", + "code": "PT" + }, + { + "name": "Puerto Rico", + "code": "PR" + }, + { + "name": "Qatar", + "code": "QA" + }, + { + "name": "Reunion", + "code": "RE" + }, + { + "name": "Romania", + "code": "RO" + }, + { + "name": "Russian Federation", + "code": "RU" + }, + { + "name": "Rwanda", + "code": "RW" + }, + { + "name": "Saint Barthélemy", + "code": "BL" + }, + { + "name": "Saint Helena", + "code": "SH" + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN" + }, + { + "name": "Saint Lucia", + "code": "LC" + }, + { + "name": "Saint Martin", + "code": "MF" + }, + { + "name": "Saint Pierre and Miquelon", + "code": "PM" + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC" + }, + { + "name": "Samoa", + "code": "WS" + }, + { + "name": "San Marino", + "code": "SM" + }, + { + "name": "Sao Tome and Principe", + "code": "ST" + }, + { + "name": "Saudi Arabia", + "code": "SA" + }, + { + "name": "Senegal", + "code": "SN" + }, + { + "name": "Serbia", + "code": "RS" + }, + { + "name": "Serbia and Montenegro", + "code": "CS" + }, + { + "name": "Seychelles", + "code": "SC" + }, + { + "name": "Sierra Leone", + "code": "SL" + }, + { + "name": "Singapore", + "code": "SG" + }, + { + "name": "Sint Maarten", + "code": "SX" + }, + { + "name": "Slovakia", + "code": "SK" + }, + { + "name": "Slovenia", + "code": "SI" + }, + { + "name": "Solomon Islands", + "code": "SB" + }, + { + "name": "Somalia", + "code": "SO" + }, + { + "name": "South Africa", + "code": "ZA" + }, + { + "name": "South Georgia and the South Sandwich Islands", + "code": "GS" + }, + { + "name": "South Sudan", + "code": "SS" + }, + { + "name": "Spain", + "code": "ES" + }, + { + "name": "Sri Lanka", + "code": "LK" + }, + { + "name": "Sudan", + "code": "SD" + }, + { + "name": "Suriname", + "code": "SR" + }, + { + "name": "Svalbard and Jan Mayen", + "code": "SJ" + }, + { + "name": "Swaziland", + "code": "SZ" + }, + { + "name": "Sweden", + "code": "SE" + }, + { + "name": "Switzerland", + "code": "CH" + }, + { + "name": "Syrian Arab Republic", + "code": "SY" + }, + { + "name": "Taiwan", + "code": "TW" + }, + { + "name": "Tajikistan", + "code": "TJ" + }, + { + "name": "Tanzania, United Republic of", + "code": "TZ" + }, + { + "name": "Thailand", + "code": "TH" + }, + { + "name": "Timor-Leste", + "code": "TL" + }, + { + "name": "Togo", + "code": "TG" + }, + { + "name": "Tokelau", + "code": "TK" + }, + { + "name": "Tonga", + "code": "TO" + }, + { + "name": "Trinidad and Tobago", + "code": "TT" + }, + { + "name": "Tunisia", + "code": "TN" + }, + { + "name": "Turkey", + "code": "TR" + }, + { + "name": "Turkmenistan", + "code": "TM" + }, + { + "name": "Turks and Caicos Islands", + "code": "TC" + }, + { + "name": "Tuvalu", + "code": "TV" + }, + { + "name": "Uganda", + "code": "UG" + }, + { + "name": "Ukraine", + "code": "UA" + }, + { + "name": "United Arab Emirates", + "code": "AE" + }, + { + "name": "United Kingdom", + "code": "GB" + }, + { + "name": "United States", + "code": "US" + }, + { + "name": "United States Minor Outlying Islands", + "code": "UM" + }, + { + "name": "Uruguay", + "code": "UY" + }, + { + "name": "Uzbekistan", + "code": "UZ" + }, + { + "name": "Vanuatu", + "code": "VU" + }, + { + "name": "Venezuela", + "code": "VE" + }, + { + "name": "Vietnam", + "code": "VN" + }, + { + "name": "Virgin Islands, British", + "code": "VG" + }, + { + "name": "Virgin Islands, U.S.", + "code": "VI" + }, + { + "name": "Wallis and Futuna", + "code": "WF" + }, + { + "name": "Western Sahara", + "code": "EH" + }, + { + "name": "Yemen", + "code": "YE" + }, + { + "name": "Zambia", + "code": "ZM" + }, + { + "name": "Zimbabwe", + "code": "ZW" + }, + { + "name": "Åland Islands", + "code": "AX" + } +]; \ No newline at end of file diff --git a/server/db/maxmind.ts b/server/db/maxmind.ts new file mode 100644 index 00000000..ca398df2 --- /dev/null +++ b/server/db/maxmind.ts @@ -0,0 +1,13 @@ +import maxmind, { CountryResponse, Reader } from "maxmind"; +import config from "@server/lib/config"; + +let maxmindLookup: Reader | null; +if (config.getRawConfig().server.maxmind_db_path) { + maxmindLookup = await maxmind.open( + config.getRawConfig().server.maxmind_db_path! + ); +} else { + maxmindLookup = null; +} + +export { maxmindLookup }; diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index c7c292f0..6dbef7e8 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -7,9 +7,22 @@ function createDb() { const config = readConfigFile(); if (!config.postgres) { - throw new Error( - "Postgres configuration is missing in the configuration file." - ); + // check the environment variables for postgres config + if (process.env.POSTGRES_CONNECTION_STRING) { + config.postgres = { + connection_string: process.env.POSTGRES_CONNECTION_STRING + }; + if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { + const replicas = process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map((conn) => ({ + connection_string: conn.trim() + })); + config.postgres.replicas = replicas; + } + } else { + throw new Error( + "Postgres configuration is missing in the configuration file." + ); + } } const connectionString = config.postgres?.connection_string; @@ -22,11 +35,12 @@ function createDb() { } // Create connection pools instead of individual connections + const poolConfig = config.postgres.pool; const primaryPool = new Pool({ connectionString, - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, + max: poolConfig?.max_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, }); const replicas = []; @@ -37,9 +51,9 @@ function createDb() { for (const conn of replicaConnections) { const replicaPool = new Pool({ connectionString: conn.connection_string, - max: 10, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, + max: poolConfig?.max_replica_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, }); replicas.push(DrizzlePostgres(replicaPool)); } diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts index 4829c04c..6e2c79f5 100644 --- a/server/db/pg/index.ts +++ b/server/db/pg/index.ts @@ -1,2 +1,3 @@ export * from "./driver"; -export * from "./schema"; \ No newline at end of file +export * from "./schema/schema"; +export * from "./schema/privateSchema"; diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts new file mode 100644 index 00000000..67fb28ec --- /dev/null +++ b/server/db/pg/schema/privateSchema.ts @@ -0,0 +1,232 @@ +import { + pgTable, + serial, + varchar, + boolean, + integer, + bigint, + real, + text +} from "drizzle-orm/pg-core"; +import { InferSelectModel } from "drizzle-orm"; +import { domains, orgs, targets, users, exitNodes, sessions } from "./schema"; + +export const certificates = pgTable("certificates", { + certId: serial("certId").primaryKey(), + domain: varchar("domain", { length: 255 }).notNull().unique(), + domainId: varchar("domainId").references(() => domains.domainId, { + onDelete: "cascade" + }), + wildcard: boolean("wildcard").default(false), + status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed + expiresAt: bigint("expiresAt", { mode: "number" }), + lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }), + createdAt: bigint("createdAt", { mode: "number" }).notNull(), + updatedAt: bigint("updatedAt", { mode: "number" }).notNull(), + orderId: varchar("orderId", { length: 500 }), + errorMessage: text("errorMessage"), + renewalCount: integer("renewalCount").default(0), + certFile: text("certFile"), + keyFile: text("keyFile") +}); + +export const dnsChallenge = pgTable("dnsChallenges", { + dnsChallengeId: serial("dnsChallengeId").primaryKey(), + domain: varchar("domain", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + keyAuthorization: varchar("keyAuthorization", { length: 1000 }).notNull(), + createdAt: bigint("createdAt", { mode: "number" }).notNull(), + expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), + completed: boolean("completed").default(false) +}); + +export const account = pgTable("account", { + accountId: serial("accountId").primaryKey(), + userId: varchar("userId") + .notNull() + .references(() => users.userId, { onDelete: "cascade" }) +}); + +export const customers = pgTable("customers", { + customerId: varchar("customerId", { length: 255 }).primaryKey().notNull(), + orgId: varchar("orgId", { length: 255 }) + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + // accountId: integer("accountId") + // .references(() => account.accountId, { onDelete: "cascade" }), // Optional, if using accounts + email: varchar("email", { length: 255 }), + name: varchar("name", { length: 255 }), + phone: varchar("phone", { length: 50 }), + address: text("address"), + createdAt: bigint("createdAt", { mode: "number" }).notNull(), + updatedAt: bigint("updatedAt", { mode: "number" }).notNull() +}); + +export const subscriptions = pgTable("subscriptions", { + subscriptionId: varchar("subscriptionId", { length: 255 }) + .primaryKey() + .notNull(), + customerId: varchar("customerId", { length: 255 }) + .notNull() + .references(() => customers.customerId, { onDelete: "cascade" }), + status: varchar("status", { length: 50 }).notNull().default("active"), // active, past_due, canceled, unpaid + canceledAt: bigint("canceledAt", { mode: "number" }), + createdAt: bigint("createdAt", { mode: "number" }).notNull(), + updatedAt: bigint("updatedAt", { mode: "number" }), + billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }) +}); + +export const subscriptionItems = pgTable("subscriptionItems", { + subscriptionItemId: serial("subscriptionItemId").primaryKey(), + subscriptionId: varchar("subscriptionId", { length: 255 }) + .notNull() + .references(() => subscriptions.subscriptionId, { + onDelete: "cascade" + }), + planId: varchar("planId", { length: 255 }).notNull(), + priceId: varchar("priceId", { length: 255 }), + meterId: varchar("meterId", { length: 255 }), + unitAmount: real("unitAmount"), + tiers: text("tiers"), + interval: varchar("interval", { length: 50 }), + currentPeriodStart: bigint("currentPeriodStart", { mode: "number" }), + currentPeriodEnd: bigint("currentPeriodEnd", { mode: "number" }), + name: varchar("name", { length: 255 }) +}); + +export const accountDomains = pgTable("accountDomains", { + accountId: integer("accountId") + .notNull() + .references(() => account.accountId, { onDelete: "cascade" }), + domainId: varchar("domainId") + .notNull() + .references(() => domains.domainId, { onDelete: "cascade" }) +}); + +export const usage = pgTable("usage", { + usageId: varchar("usageId", { length: 255 }).primaryKey(), + featureId: varchar("featureId", { length: 255 }).notNull(), + orgId: varchar("orgId") + .references(() => orgs.orgId, { onDelete: "cascade" }) + .notNull(), + meterId: varchar("meterId", { length: 255 }), + instantaneousValue: real("instantaneousValue"), + latestValue: real("latestValue").notNull(), + previousValue: real("previousValue"), + updatedAt: bigint("updatedAt", { mode: "number" }).notNull(), + rolledOverAt: bigint("rolledOverAt", { mode: "number" }), + nextRolloverAt: bigint("nextRolloverAt", { mode: "number" }) +}); + +export const limits = pgTable("limits", { + limitId: varchar("limitId", { length: 255 }).primaryKey(), + featureId: varchar("featureId", { length: 255 }).notNull(), + orgId: varchar("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), + value: real("value"), + description: text("description") +}); + +export const usageNotifications = pgTable("usageNotifications", { + notificationId: serial("notificationId").primaryKey(), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + featureId: varchar("featureId", { length: 255 }).notNull(), + limitId: varchar("limitId", { length: 255 }).notNull(), + notificationType: varchar("notificationType", { length: 50 }).notNull(), + sentAt: bigint("sentAt", { mode: "number" }).notNull() +}); + +export const domainNamespaces = pgTable("domainNamespaces", { + domainNamespaceId: varchar("domainNamespaceId", { + length: 255 + }).primaryKey(), + domainId: varchar("domainId") + .references(() => domains.domainId, { + onDelete: "set null" + }) + .notNull() +}); + +export const exitNodeOrgs = pgTable("exitNodeOrgs", { + exitNodeId: integer("exitNodeId") + .notNull() + .references(() => exitNodes.exitNodeId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + +export const remoteExitNodes = pgTable("remoteExitNode", { + remoteExitNodeId: varchar("id").primaryKey(), + secretHash: varchar("secretHash").notNull(), + dateCreated: varchar("dateCreated").notNull(), + version: varchar("version"), + exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { + onDelete: "cascade" + }) +}); + +export const remoteExitNodeSessions = pgTable("remoteExitNodeSession", { + sessionId: varchar("id").primaryKey(), + remoteExitNodeId: varchar("remoteExitNodeId") + .notNull() + .references(() => remoteExitNodes.remoteExitNodeId, { + onDelete: "cascade" + }), + expiresAt: bigint("expiresAt", { mode: "number" }).notNull() +}); + +export const loginPage = pgTable("loginPage", { + loginPageId: serial("loginPageId").primaryKey(), + subdomain: varchar("subdomain"), + fullDomain: varchar("fullDomain"), + exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { + onDelete: "set null" + }), + domainId: varchar("domainId").references(() => domains.domainId, { + onDelete: "set null" + }) +}); + +export const loginPageOrg = pgTable("loginPageOrg", { + loginPageId: integer("loginPageId") + .notNull() + .references(() => loginPage.loginPageId, { onDelete: "cascade" }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + +export const sessionTransferToken = pgTable("sessionTransferToken", { + token: varchar("token").primaryKey(), + sessionId: varchar("sessionId") + .notNull() + .references(() => sessions.sessionId, { + onDelete: "cascade" + }), + encryptedSession: text("encryptedSession").notNull(), + expiresAt: bigint("expiresAt", { mode: "number" }).notNull() +}); + +export type Limit = InferSelectModel; +export type Account = InferSelectModel; +export type Certificate = InferSelectModel; +export type DnsChallenge = InferSelectModel; +export type Customer = InferSelectModel; +export type Subscription = InferSelectModel; +export type SubscriptionItem = InferSelectModel; +export type Usage = InferSelectModel; +export type UsageLimit = InferSelectModel; +export type AccountDomain = InferSelectModel; +export type UsageNotification = InferSelectModel; +export type RemoteExitNode = InferSelectModel; +export type RemoteExitNodeSession = InferSelectModel< + typeof remoteExitNodeSessions +>; +export type ExitNodeOrg = InferSelectModel; +export type LoginPage = InferSelectModel; diff --git a/server/db/pg/schema.ts b/server/db/pg/schema/schema.ts similarity index 91% rename from server/db/pg/schema.ts rename to server/db/pg/schema/schema.ts index 3cb5486b..2e307c5f 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -9,6 +9,7 @@ import { text } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; +import { randomUUID } from "crypto"; export const domains = pgTable("domains", { domainId: varchar("domainId").primaryKey(), @@ -24,7 +25,8 @@ export const orgs = pgTable("orgs", { orgId: varchar("orgId").primaryKey(), name: varchar("name").notNull(), subnet: varchar("subnet"), - createdAt: text("createdAt") + createdAt: text("createdAt"), + settings: text("settings") // JSON blob of org-specific settings }); export const orgDomains = pgTable("orgDomains", { @@ -66,6 +68,10 @@ export const sites = pgTable("sites", { export const resources = pgTable("resources", { resourceId: serial("resourceId").primaryKey(), + resourceGuid: varchar("resourceGuid", { length: 36 }) + .unique() + .notNull() + .$defaultFn(() => randomUUID()), orgId: varchar("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -96,7 +102,7 @@ export const resources = pgTable("resources", { skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { onDelete: "cascade" }), - headers: text("headers"), // comma-separated list of headers to add to the request + headers: text("headers") // comma-separated list of headers to add to the request }); export const targets = pgTable("targets", { @@ -118,6 +124,30 @@ export const targets = pgTable("targets", { enabled: boolean("enabled").notNull().default(true), path: text("path"), pathMatchType: text("pathMatchType"), // exact, prefix, regex + rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target + rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix + priority: integer("priority").notNull().default(100) +}); + +export const targetHealthCheck = pgTable("targetHealthCheck", { + targetHealthCheckId: serial("targetHealthCheckId").primaryKey(), + targetId: integer("targetId") + .notNull() + .references(() => targets.targetId, { onDelete: "cascade" }), + hcEnabled: boolean("hcEnabled").notNull().default(false), + hcPath: varchar("hcPath"), + hcScheme: varchar("hcScheme"), + hcMode: varchar("hcMode").default("http"), + hcHostname: varchar("hcHostname"), + hcPort: integer("hcPort"), + hcInterval: integer("hcInterval").default(30), // in seconds + hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds + hcTimeout: integer("hcTimeout").default(5), // in seconds + hcHeaders: varchar("hcHeaders"), + hcFollowRedirects: boolean("hcFollowRedirects").default(true), + hcMethod: varchar("hcMethod").default("GET"), + hcStatus: integer("hcStatus"), // http code + hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy" }); export const exitNodes = pgTable("exitNodes", { @@ -135,7 +165,8 @@ export const exitNodes = pgTable("exitNodes", { region: varchar("region") }); -export const siteResources = pgTable("siteResources", { // this is for the clients +export const siteResources = pgTable("siteResources", { + // this is for the clients siteResourceId: serial("siteResourceId").primaryKey(), siteId: integer("siteId") .notNull() @@ -149,7 +180,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien proxyPort: integer("proxyPort").notNull(), destinationPort: integer("destinationPort").notNull(), destinationIp: varchar("destinationIp").notNull(), - enabled: boolean("enabled").notNull().default(true), + enabled: boolean("enabled").notNull().default(true) }); export const users = pgTable("user", { @@ -350,6 +381,14 @@ export const resourcePassword = pgTable("resourcePassword", { passwordHash: varchar("passwordHash").notNull() }); +export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { + headerAuthId: serial("headerAuthId").primaryKey(), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + headerAuthHash: varchar("headerAuthHash").notNull() +}); + export const resourceAccessToken = pgTable("resourceAccessToken", { accessTokenId: varchar("accessTokenId").primaryKey(), orgId: varchar("orgId") @@ -659,6 +698,7 @@ export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceHeaderAuth = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; @@ -680,3 +720,5 @@ export type OrgDomains = InferSelectModel; export type SiteResource = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; +export type TargetHealthCheck = InferSelectModel; +export type IdpOidcConfig = InferSelectModel; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index eefd11f7..9deaddca 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -1,4 +1,4 @@ -import { db, RoleResource, UserOrg } from "@server/db"; +import { db, loginPage, LoginPage, loginPageOrg } from "@server/db"; import { Resource, ResourcePassword, @@ -6,6 +6,8 @@ import { ResourceRule, resourcePassword, resourcePincode, + resourceHeaderAuth, + ResourceHeaderAuth, resourceRules, resources, roleResources, @@ -15,15 +17,12 @@ import { users } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; -import axios from "axios"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import { tokenManager } from "@server/lib/tokenManager"; export type ResourceWithAuth = { resource: Resource | null; pincode: ResourcePincode | null; password: ResourcePassword | null; + headerAuth: ResourceHeaderAuth | null; }; export type UserSessionWithUser = { @@ -37,30 +36,6 @@ export type UserSessionWithUser = { export async function getResourceByDomain( domain: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const [result] = await db .select() .from(resources) @@ -72,6 +47,10 @@ export async function getResourceByDomain( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -82,7 +61,8 @@ export async function getResourceByDomain( return { resource: result.resources, pincode: result.resourcePincode, - password: result.resourcePassword + password: result.resourcePassword, + headerAuth: result.resourceHeaderAuth }; } @@ -92,30 +72,6 @@ export async function getResourceByDomain( export async function getUserSessionWithUser( userSessionId: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const [res] = await db .select() .from(sessions) @@ -139,30 +95,6 @@ export async function getUserOrgRoles( userId: string, orgId: string ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return []; - } - } - const userOrgRes = await db .select() .from(userOrgs) @@ -176,42 +108,7 @@ export async function getUserOrgRoles( export async function getRoleResourceAccess( resourceId: number, roleIds: number[] -): Promise { - if (config.isManagedMode()) { - const requests = roleIds.map(async (roleId) => { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - }); - const results = await Promise.allSettled(requests); - - for (const result of results) { - if (result.status === "fulfilled") { - if (result.value) return result.value; - } else { - const error = result.reason; - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error( - "Error fetching config in verify session:", - error - ); - } - } - } - return null; - } - +) { const roleResourceAccess = await db .select() .from(roleResources) @@ -233,30 +130,6 @@ export async function getUserResourceAccess( userId: string, resourceId: number ) { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return null; - } - } - const userResourceAccess = await db .select() .from(userResources) @@ -277,30 +150,6 @@ export async function getUserResourceAccess( export async function getResourceRules( resourceId: number ): Promise { - if (config.isManagedMode()) { - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, - await tokenManager.getAuthHeader() - ); - return response.data.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } - return []; - } - } - const rules = await db .select() .from(resourceRules) @@ -308,3 +157,26 @@ export async function getResourceRules( return rules; } + +/** + * Get organization login page + */ +export async function getOrgLoginPage( + orgId: string +): Promise { + const [result] = await db + .select() + .from(loginPageOrg) + .where(eq(loginPageOrg.orgId, orgId)) + .innerJoin( + loginPage, + eq(loginPageOrg.loginPageId, loginPage.loginPageId) + ) + .limit(1); + + if (!result) { + return null; + } + + return result?.loginPage; +} diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 6369c268..211ba8ea 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -1,6 +1,6 @@ import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; -import * as schema from "./schema"; +import * as schema from "./schema/schema"; import path from "path"; import fs from "fs"; import { APP_PATH } from "@server/lib/consts"; diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts index 9ad4678c..6e2c79f5 100644 --- a/server/db/sqlite/index.ts +++ b/server/db/sqlite/index.ts @@ -1,2 +1,3 @@ export * from "./driver"; -export * from "./schema"; +export * from "./schema/schema"; +export * from "./schema/privateSchema"; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts new file mode 100644 index 00000000..557ebfd6 --- /dev/null +++ b/server/db/sqlite/schema/privateSchema.ts @@ -0,0 +1,226 @@ +import { + sqliteTable, + integer, + text, + real +} from "drizzle-orm/sqlite-core"; +import { InferSelectModel } from "drizzle-orm"; +import { domains, orgs, targets, users, exitNodes, sessions } from "./schema"; + +export const certificates = sqliteTable("certificates", { + certId: integer("certId").primaryKey({ autoIncrement: true }), + domain: text("domain").notNull().unique(), + domainId: text("domainId").references(() => domains.domainId, { + onDelete: "cascade" + }), + wildcard: integer("wildcard", { mode: "boolean" }).default(false), + status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed + expiresAt: integer("expiresAt"), + lastRenewalAttempt: integer("lastRenewalAttempt"), + createdAt: integer("createdAt").notNull(), + updatedAt: integer("updatedAt").notNull(), + orderId: text("orderId"), + errorMessage: text("errorMessage"), + renewalCount: integer("renewalCount").default(0), + certFile: text("certFile"), + keyFile: text("keyFile") +}); + +export const dnsChallenge = sqliteTable("dnsChallenges", { + dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }), + domain: text("domain").notNull(), + token: text("token").notNull(), + keyAuthorization: text("keyAuthorization").notNull(), + createdAt: integer("createdAt").notNull(), + expiresAt: integer("expiresAt").notNull(), + completed: integer("completed", { mode: "boolean" }).default(false) +}); + +export const account = sqliteTable("account", { + accountId: integer("accountId").primaryKey({ autoIncrement: true }), + userId: text("userId") + .notNull() + .references(() => users.userId, { onDelete: "cascade" }) +}); + +export const customers = sqliteTable("customers", { + customerId: text("customerId").primaryKey().notNull(), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + // accountId: integer("accountId") + // .references(() => account.accountId, { onDelete: "cascade" }), // Optional, if using accounts + email: text("email"), + name: text("name"), + phone: text("phone"), + address: text("address"), + createdAt: integer("createdAt").notNull(), + updatedAt: integer("updatedAt").notNull() +}); + +export const subscriptions = sqliteTable("subscriptions", { + subscriptionId: text("subscriptionId") + .primaryKey() + .notNull(), + customerId: text("customerId") + .notNull() + .references(() => customers.customerId, { onDelete: "cascade" }), + status: text("status").notNull().default("active"), // active, past_due, canceled, unpaid + canceledAt: integer("canceledAt"), + createdAt: integer("createdAt").notNull(), + updatedAt: integer("updatedAt"), + billingCycleAnchor: integer("billingCycleAnchor") +}); + +export const subscriptionItems = sqliteTable("subscriptionItems", { + subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }), + subscriptionId: text("subscriptionId") + .notNull() + .references(() => subscriptions.subscriptionId, { + onDelete: "cascade" + }), + planId: text("planId").notNull(), + priceId: text("priceId"), + meterId: text("meterId"), + unitAmount: real("unitAmount"), + tiers: text("tiers"), + interval: text("interval"), + currentPeriodStart: integer("currentPeriodStart"), + currentPeriodEnd: integer("currentPeriodEnd"), + name: text("name") +}); + +export const accountDomains = sqliteTable("accountDomains", { + accountId: integer("accountId") + .notNull() + .references(() => account.accountId, { onDelete: "cascade" }), + domainId: text("domainId") + .notNull() + .references(() => domains.domainId, { onDelete: "cascade" }) +}); + +export const usage = sqliteTable("usage", { + usageId: text("usageId").primaryKey(), + featureId: text("featureId").notNull(), + orgId: text("orgId") + .references(() => orgs.orgId, { onDelete: "cascade" }) + .notNull(), + meterId: text("meterId"), + instantaneousValue: real("instantaneousValue"), + latestValue: real("latestValue").notNull(), + previousValue: real("previousValue"), + updatedAt: integer("updatedAt").notNull(), + rolledOverAt: integer("rolledOverAt"), + nextRolloverAt: integer("nextRolloverAt") +}); + +export const limits = sqliteTable("limits", { + limitId: text("limitId").primaryKey(), + featureId: text("featureId").notNull(), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), + value: real("value"), + description: text("description") +}); + +export const usageNotifications = sqliteTable("usageNotifications", { + notificationId: integer("notificationId").primaryKey({ autoIncrement: true }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + featureId: text("featureId").notNull(), + limitId: text("limitId").notNull(), + notificationType: text("notificationType").notNull(), + sentAt: integer("sentAt").notNull() +}); + +export const domainNamespaces = sqliteTable("domainNamespaces", { + domainNamespaceId: text("domainNamespaceId").primaryKey(), + domainId: text("domainId") + .references(() => domains.domainId, { + onDelete: "set null" + }) + .notNull() +}); + +export const exitNodeOrgs = sqliteTable("exitNodeOrgs", { + exitNodeId: integer("exitNodeId") + .notNull() + .references(() => exitNodes.exitNodeId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + +export const remoteExitNodes = sqliteTable("remoteExitNode", { + remoteExitNodeId: text("id").primaryKey(), + secretHash: text("secretHash").notNull(), + dateCreated: text("dateCreated").notNull(), + version: text("version"), + exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { + onDelete: "cascade" + }) +}); + +export const remoteExitNodeSessions = sqliteTable("remoteExitNodeSession", { + sessionId: text("id").primaryKey(), + remoteExitNodeId: text("remoteExitNodeId") + .notNull() + .references(() => remoteExitNodes.remoteExitNodeId, { + onDelete: "cascade" + }), + expiresAt: integer("expiresAt").notNull() +}); + +export const loginPage = sqliteTable("loginPage", { + loginPageId: integer("loginPageId").primaryKey({ autoIncrement: true }), + subdomain: text("subdomain"), + fullDomain: text("fullDomain"), + exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { + onDelete: "set null" + }), + domainId: text("domainId").references(() => domains.domainId, { + onDelete: "set null" + }) +}); + +export const loginPageOrg = sqliteTable("loginPageOrg", { + loginPageId: integer("loginPageId") + .notNull() + .references(() => loginPage.loginPageId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + +export const sessionTransferToken = sqliteTable("sessionTransferToken", { + token: text("token").primaryKey(), + sessionId: text("sessionId") + .notNull() + .references(() => sessions.sessionId, { + onDelete: "cascade" + }), + encryptedSession: text("encryptedSession").notNull(), + expiresAt: integer("expiresAt").notNull() +}); + +export type Limit = InferSelectModel; +export type Account = InferSelectModel; +export type Certificate = InferSelectModel; +export type DnsChallenge = InferSelectModel; +export type Customer = InferSelectModel; +export type Subscription = InferSelectModel; +export type SubscriptionItem = InferSelectModel; +export type Usage = InferSelectModel; +export type UsageLimit = InferSelectModel; +export type AccountDomain = InferSelectModel; +export type UsageNotification = InferSelectModel; +export type RemoteExitNode = InferSelectModel; +export type RemoteExitNodeSession = InferSelectModel< + typeof remoteExitNodeSessions +>; +export type ExitNodeOrg = InferSelectModel; +export type LoginPage = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema/schema.ts similarity index 92% rename from server/db/sqlite/schema.ts rename to server/db/sqlite/schema/schema.ts index c623fae3..124a20be 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; @@ -17,7 +18,8 @@ export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), subnet: text("subnet"), - createdAt: text("createdAt") + createdAt: text("createdAt"), + settings: text("settings") // JSON blob of org-specific settings }); export const userDomains = sqliteTable("userDomains", { @@ -72,6 +74,10 @@ export const sites = sqliteTable("sites", { export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), + resourceGuid: text("resourceGuid", { length: 36 }) + .unique() + .notNull() + .$defaultFn(() => randomUUID()), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -108,7 +114,7 @@ export const resources = sqliteTable("resources", { skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { onDelete: "cascade" }), - headers: text("headers"), // comma-separated list of headers to add to the request + headers: text("headers") // comma-separated list of headers to add to the request }); export const targets = sqliteTable("targets", { @@ -130,6 +136,30 @@ export const targets = sqliteTable("targets", { enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), path: text("path"), pathMatchType: text("pathMatchType"), // exact, prefix, regex + rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target + rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix + priority: integer("priority").notNull().default(100) +}); + +export const targetHealthCheck = sqliteTable("targetHealthCheck", { + targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ autoIncrement: true }), + targetId: integer("targetId") + .notNull() + .references(() => targets.targetId, { onDelete: "cascade" }), + hcEnabled: integer("hcEnabled", { mode: "boolean" }).notNull().default(false), + hcPath: text("hcPath"), + hcScheme: text("hcScheme"), + hcMode: text("hcMode").default("http"), + hcHostname: text("hcHostname"), + hcPort: integer("hcPort"), + hcInterval: integer("hcInterval").default(30), // in seconds + hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds + hcTimeout: integer("hcTimeout").default(5), // in seconds + hcHeaders: text("hcHeaders"), + hcFollowRedirects: integer("hcFollowRedirects", { mode: "boolean" }).default(true), + hcMethod: text("hcMethod").default("GET"), + hcStatus: integer("hcStatus"), // http code + hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy" }); export const exitNodes = sqliteTable("exitNodes", { @@ -450,18 +480,6 @@ export const userResources = sqliteTable("userResources", { .references(() => resources.resourceId, { onDelete: "cascade" }) }); -export const limitsTable = sqliteTable("limits", { - limitId: integer("limitId").primaryKey({ autoIncrement: true }), - orgId: text("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - name: text("name").notNull(), - value: integer("value").notNull(), - description: text("description") -}); - export const userInvites = sqliteTable("userInvites", { inviteId: text("inviteId").primaryKey(), orgId: text("orgId") @@ -496,6 +514,16 @@ export const resourcePassword = sqliteTable("resourcePassword", { passwordHash: text("passwordHash").notNull() }); +export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { + headerAuthId: integer("headerAuthId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + headerAuthHash: text("headerAuthHash").notNull() +}); + export const resourceAccessToken = sqliteTable("resourceAccessToken", { accessTokenId: text("accessTokenId").primaryKey(), orgId: text("orgId") @@ -691,12 +719,12 @@ export type RoleSite = InferSelectModel; export type UserSite = InferSelectModel; export type RoleResource = InferSelectModel; export type UserResource = InferSelectModel; -export type Limit = InferSelectModel; export type UserInvite = InferSelectModel; export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceHeaderAuth = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; @@ -716,3 +744,5 @@ export type SiteResource = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; +export type TargetHealthCheck = InferSelectModel; +export type IdpOidcConfig = InferSelectModel; diff --git a/server/emails/index.ts b/server/emails/index.ts index 2cdef8a1..42cfa39c 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -6,11 +6,6 @@ import logger from "@server/logger"; import SMTPTransport from "nodemailer/lib/smtp-transport"; function createEmailClient() { - if (config.isManagedMode()) { - // LETS NOT WORRY ABOUT EMAILS IN HYBRID - return; - } - const emailConfig = config.getRawConfig().email; if (!emailConfig) { logger.warn( diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index 9b99d18e..c8a0b077 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -2,7 +2,6 @@ import { render } from "@react-email/render"; import { ReactElement } from "react"; import emailClient from "@server/emails"; import logger from "@server/logger"; -import config from "@server/lib/config"; export async function sendEmail( template: ReactElement, @@ -11,7 +10,7 @@ export async function sendEmail( from: string | undefined; to: string | undefined; subject: string; - }, + } ) { if (!emailClient) { logger.warn("Email client not configured, skipping email send"); @@ -25,16 +24,16 @@ export async function sendEmail( const emailHtml = await render(template); - const appName = "Pangolin"; + const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config await emailClient.sendMail({ from: { name: opts.name || appName, - address: opts.from, + address: opts.from }, to: opts.to, subject: opts.subject, - html: emailHtml, + html: emailHtml }); } diff --git a/server/emails/templates/NotifyUsageLimitApproaching.tsx b/server/emails/templates/NotifyUsageLimitApproaching.tsx new file mode 100644 index 00000000..beab0300 --- /dev/null +++ b/server/emails/templates/NotifyUsageLimitApproaching.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { themeColors } from "./lib/theme"; +import { + EmailContainer, + EmailFooter, + EmailGreeting, + EmailHeading, + EmailLetterHead, + EmailSignature, + EmailText +} from "./components/Email"; + +interface Props { + email: string; + limitName: string; + currentUsage: number; + usageLimit: number; + billingLink: string; // Link to billing page +} + +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); + + return ( + + + {previewText} + + + + + + Usage Limit Warning + + Hi there, + + + We wanted to let you know that your usage for {limitName} is approaching your plan limit. + + + + 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. + + + + 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. + + + + + + + + + + ); +}; + +export default NotifyUsageLimitApproaching; diff --git a/server/emails/templates/NotifyUsageLimitReached.tsx b/server/emails/templates/NotifyUsageLimitReached.tsx new file mode 100644 index 00000000..783d1b0e --- /dev/null +++ b/server/emails/templates/NotifyUsageLimitReached.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { themeColors } from "./lib/theme"; +import { + EmailContainer, + EmailFooter, + EmailGreeting, + EmailHeading, + EmailLetterHead, + EmailSignature, + EmailText +} from "./components/Email"; + +interface Props { + email: string; + limitName: string; + currentUsage: number; + usageLimit: number; + billingLink: string; // Link to billing page +} + +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); + + return ( + + + {previewText} + + + + + + Usage Limit Reached - Action Required + + Hi there, + + + You have reached your usage limit for {limitName}. + + + + 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. + + + + What you can do: +
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. + + + + + +
+ +
+ + ); +}; + +export default NotifyUsageLimitReached; diff --git a/server/emails/templates/components/Email.tsx b/server/emails/templates/components/Email.tsx index ef5c37f8..1ebd40e0 100644 --- a/server/emails/templates/components/Email.tsx +++ b/server/emails/templates/components/Email.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Container, Img } from "@react-email/components"; -import { build } from "@server/build"; // EmailContainer: Wraps the entire email layout export function EmailContainer({ children }: { children: React.ReactNode }) { @@ -17,7 +16,7 @@ export function EmailLetterHead() {
Fossorial - {build === "saas" && ( + {false && (
{children}

@@ -107,7 +106,7 @@ export function EmailSignature() {

Best regards,
- The Fossorial Team + The Pangolin Team

); diff --git a/server/hybridServer.ts b/server/hybridServer.ts deleted file mode 100644 index bb26489d..00000000 --- a/server/hybridServer.ts +++ /dev/null @@ -1,151 +0,0 @@ -import logger from "@server/logger"; -import config from "@server/lib/config"; -import { createWebSocketClient } from "./routers/ws/client"; -import { addPeer, deletePeer } from "./routers/gerbil/peers"; -import { db, exitNodes } from "./db"; -import { TraefikConfigManager } from "./lib/traefikConfig"; -import { tokenManager } from "./lib/tokenManager"; -import { APP_VERSION } from "./lib/consts"; -import axios from "axios"; - -export async function createHybridClientServer() { - logger.info("Starting hybrid client server..."); - - // Start the token manager - await tokenManager.start(); - - const token = await tokenManager.getToken(); - - const monitor = new TraefikConfigManager(); - - await monitor.start(); - - // Create client - const client = createWebSocketClient( - token, - config.getRawConfig().managed!.endpoint!, - { - reconnectInterval: 5000, - pingInterval: 30000, - pingTimeout: 10000 - } - ); - - // Register message handlers - client.registerHandler("remoteExitNode/peers/add", async (message) => { - const { publicKey, allowedIps } = message.data; - - // TODO: we are getting the exit node twice here - // NOTE: there should only be one gerbil registered so... - const [exitNode] = await db.select().from(exitNodes).limit(1); - await addPeer(exitNode.exitNodeId, { - publicKey: publicKey, - allowedIps: allowedIps || [] - }); - }); - - client.registerHandler("remoteExitNode/peers/remove", async (message) => { - const { publicKey } = message.data; - - // TODO: we are getting the exit node twice here - // NOTE: there should only be one gerbil registered so... - const [exitNode] = await db.select().from(exitNodes).limit(1); - await deletePeer(exitNode.exitNodeId, publicKey); - }); - - // /update-proxy-mapping - client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => { - try { - const [exitNode] = await db.select().from(exitNodes).limit(1); - if (!exitNode) { - logger.error("No exit node found for proxy mapping update"); - return; - } - - const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data); - logger.info(`Successfully updated proxy mapping: ${response.status}`); - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error updating proxy mapping:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating proxy mapping:", error); - } - } - }); - - // /update-destinations - client.registerHandler("remoteExitNode/update-destinations", async (message) => { - try { - const [exitNode] = await db.select().from(exitNodes).limit(1); - if (!exitNode) { - logger.error("No exit node found for destinations update"); - return; - } - - const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data); - logger.info(`Successfully updated destinations: ${response.status}`); - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error updating destinations:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating destinations:", error); - } - } - }); - - client.registerHandler("remoteExitNode/traefik/reload", async (message) => { - await monitor.HandleTraefikConfig(); - }); - - // Listen to connection events - client.on("connect", () => { - logger.info("Connected to WebSocket server"); - client.sendMessage("remoteExitNode/register", { - remoteExitNodeVersion: APP_VERSION - }); - }); - - client.on("disconnect", () => { - logger.info("Disconnected from WebSocket server"); - }); - - client.on("message", (message) => { - logger.info( - `Received message: ${message.type} ${JSON.stringify(message.data)}` - ); - }); - - // Connect to the server - try { - await client.connect(); - logger.info("Connection initiated"); - } catch (error) { - logger.error("Failed to connect:", error); - } - - // Store the ping interval stop function for cleanup if needed - const stopPingInterval = client.sendMessageInterval( - "remoteExitNode/ping", - { timestamp: Date.now() / 1000 }, - 60000 - ); // send every minute - - // Return client and cleanup function for potential use - return { client, stopPingInterval }; -} diff --git a/server/index.ts b/server/index.ts index 5210ba5d..f80d24eb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,33 +5,36 @@ import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; -import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; +import { + ApiKey, + ApiKeyOrg, + RemoteExitNode, + Session, + User, + UserOrg +} from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; -import { createHybridClientServer } from "./hybridServer"; import config from "@server/lib/config"; import { setHostMeta } from "@server/lib/hostMeta"; -import { TraefikConfigManager } from "./lib/traefikConfig.js"; +import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js"; +import { initCleanup } from "@server/cleanup"; async function startServers() { await setHostMeta(); await config.initServer(); + await runSetupFunctions(); // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); - let hybridClientServer; let nextServer; - if (config.isManagedMode()) { - hybridClientServer = await createHybridClientServer(); - } else { - nextServer = await createNextServer(); - if (config.getRawConfig().traefik.file_mode) { - const monitor = new TraefikConfigManager(); - await monitor.start(); - } + nextServer = await createNextServer(); + if (config.getRawConfig().traefik.file_mode) { + const monitor = new TraefikConfigManager(); + await monitor.start(); } let integrationServer; @@ -39,12 +42,13 @@ async function startServers() { integrationServer = createIntegrationApiServer(); } + await initCleanup(); + return { apiServer, nextServer, internalServer, - integrationServer, - hybridClientServer + integrationServer }; } @@ -60,6 +64,7 @@ declare global { userRoleIds?: number[]; userOrgId?: string; userOrgIds?: string[]; + remoteExitNode?: RemoteExitNode; } } } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index eefaacd8..cbbea83f 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -13,6 +13,10 @@ import helmet from "helmet"; import swaggerUi from "swagger-ui-express"; import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; import { registry } from "./openApi"; +import fs from "fs"; +import path from "path"; +import { APP_PATH } from "./lib/consts"; +import yaml from "js-yaml"; const dev = process.env.ENVIRONMENT !== "prod"; const externalPort = config.getRawConfig().server.integration_port; @@ -92,7 +96,7 @@ function getOpenApiDocumentation() { const generator = new OpenApiGeneratorV3(registry.definitions); - return generator.generateDocument({ + const generated = generator.generateDocument({ openapi: "3.0.0", info: { version: "v1", @@ -100,4 +104,12 @@ function getOpenApiDocumentation() { }, servers: [{ url: "/v1" }] }); + + // convert to yaml and save to file + const outputPath = path.join(APP_PATH, "openapi.yaml"); + const yamlOutput = yaml.dump(generated); + fs.writeFileSync(outputPath, yamlOutput, "utf8"); + logger.info(`OpenAPI documentation saved to ${outputPath}`); + + return generated; } diff --git a/server/internalServer.ts b/server/internalServer.ts index 92da137f..0c7f885d 100644 --- a/server/internalServer.ts +++ b/server/internalServer.ts @@ -8,7 +8,8 @@ import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares"; -import internal from "@server/routers/internal"; +import { internalRouter } from "@server/routers/internal"; +import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; const internalPort = config.getRawConfig().server.internal_port; @@ -17,11 +18,12 @@ export function createInternalServer() { internalServer.use(helmet()); internalServer.use(cors()); + internalServer.use(stripDuplicateSesions); internalServer.use(cookieParser()); internalServer.use(express.json()); const prefix = `/api/v1`; - internalServer.use(prefix, internal); + internalServer.use(prefix, internalRouter); internalServer.use(notFoundMiddleware); internalServer.use(errorHandlerMiddleware); diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 47193420..6fac099a 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -69,9 +69,16 @@ export async function applyBlueprint( `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 ); diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index c6ab6f40..cc60f04b 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -2,12 +2,15 @@ import { domains, orgDomains, Resource, + resourceHeaderAuth, resourcePincode, resourceRules, resourceWhitelist, roleResources, roles, Target, + TargetHealthCheck, + targetHealthCheck, Transaction, userOrgs, userResources, @@ -22,6 +25,7 @@ import { TargetData } from "./types"; import logger from "@server/logger"; +import { createCertificate } from "@server/routers/certificates/createCertificate"; import { pickPort } from "@server/routers/target/helpers"; import { resourcePassword } from "@server/db"; import { hashPassword } from "@server/auth/password"; @@ -30,6 +34,7 @@ import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; export type ProxyResourcesResults = { proxyResource: Resource; targetsToUpdate: Target[]; + healthchecksToUpdate: TargetHealthCheck[]; }[]; export async function updateProxyResources( @@ -44,6 +49,7 @@ export async function updateProxyResources( config["proxy-resources"] )) { const targetsToUpdate: Target[] = []; + const healthchecksToUpdate: TargetHealthCheck[] = []; let resource: Resource; async function createTarget( // reusable function to create a target @@ -107,11 +113,43 @@ export async function updateProxyResources( enabled: targetData.enabled, internalPort: internalPortToCreate, path: targetData.path, - pathMatchType: targetData["path-match"] + pathMatchType: targetData["path-match"], + rewritePath: targetData.rewritePath, + rewritePathType: targetData["rewrite-match"], + priority: targetData.priority }) .returning(); targetsToUpdate.push(newTarget); + + const healthcheckData = targetData.healthcheck; + + const hcHeaders = healthcheckData?.headers + ? JSON.stringify(healthcheckData.headers) + : null; + + const [newHealthcheck] = await trx + .insert(targetHealthCheck) + .values({ + targetId: newTarget.targetId, + hcEnabled: healthcheckData?.enabled || false, + hcPath: healthcheckData?.path, + hcScheme: healthcheckData?.scheme, + hcMode: healthcheckData?.mode, + hcHostname: healthcheckData?.hostname, + hcPort: healthcheckData?.port, + hcInterval: healthcheckData?.interval, + hcUnhealthyInterval: healthcheckData?.unhealthyInterval, + hcTimeout: healthcheckData?.timeout, + hcHeaders: hcHeaders, + hcFollowRedirects: healthcheckData?.followRedirects, + hcMethod: healthcheckData?.method, + hcStatus: healthcheckData?.status, + hcHealth: "unknown" + }) + .returning(); + + healthchecksToUpdate.push(newHealthcheck); } // Find existing resource by niceId and orgId @@ -229,6 +267,32 @@ export async function updateProxyResources( }); } + await trx + .delete(resourceHeaderAuth) + .where( + eq( + resourceHeaderAuth.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.["basic-auth"]) { + const headerAuthUser = + resourceData.auth?.["basic-auth"]?.user; + const headerAuthPassword = + resourceData.auth?.["basic-auth"]?.password; + if (headerAuthUser && headerAuthPassword) { + const headerAuthHash = await hashPassword( + Buffer.from( + `${headerAuthUser}:${headerAuthPassword}` + ).toString("base64") + ); + await trx.insert(resourceHeaderAuth).values({ + resourceId: existingResource.resourceId, + headerAuthHash + }); + } + } + if (resourceData.auth?.["sso-roles"]) { const ssoRoles = resourceData.auth?.["sso-roles"]; await syncRoleResources( @@ -327,7 +391,10 @@ export async function updateProxyResources( port: targetData.port, enabled: targetData.enabled, path: targetData.path, - pathMatchType: targetData["path-match"] + pathMatchType: targetData["path-match"], + rewritePath: targetData.rewritePath, + rewritePathType: targetData["rewrite-match"], + priority: targetData.priority }) .where(eq(targets.targetId, existingTarget.targetId)) .returning(); @@ -356,6 +423,66 @@ export async function updateProxyResources( targetsToUpdate.push(finalUpdatedTarget); } + + const healthcheckData = targetData.healthcheck; + + const [oldHealthcheck] = await trx + .select() + .from(targetHealthCheck) + .where( + eq( + targetHealthCheck.targetId, + existingTarget.targetId + ) + ) + .limit(1); + + const hcHeaders = healthcheckData?.headers + ? JSON.stringify(healthcheckData.headers) + : null; + + const [newHealthcheck] = await trx + .update(targetHealthCheck) + .set({ + hcEnabled: healthcheckData?.enabled || false, + hcPath: healthcheckData?.path, + hcScheme: healthcheckData?.scheme, + hcMode: healthcheckData?.mode, + hcHostname: healthcheckData?.hostname, + hcPort: healthcheckData?.port, + hcInterval: healthcheckData?.interval, + hcUnhealthyInterval: + healthcheckData?.unhealthyInterval, + hcTimeout: healthcheckData?.timeout, + hcHeaders: hcHeaders, + hcFollowRedirects: healthcheckData?.followRedirects, + hcMethod: healthcheckData?.method, + hcStatus: healthcheckData?.status + }) + .where( + eq( + targetHealthCheck.targetId, + existingTarget.targetId + ) + ) + .returning(); + + if ( + checkIfHealthcheckChanged( + oldHealthcheck, + newHealthcheck + ) + ) { + healthchecksToUpdate.push(newHealthcheck); + // if the target is not already in the targetsToUpdate array, add it + if ( + !targetsToUpdate.find( + (t) => t.targetId === updatedTarget.targetId + ) + ) { + targetsToUpdate.push(updatedTarget); + } + } } else { await createTarget(existingResource.resourceId, targetData); } @@ -497,6 +624,25 @@ export async function updateProxyResources( }); } + if (resourceData.auth?.["basic-auth"]) { + const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; + const headerAuthPassword = + resourceData.auth?.["basic-auth"]?.password; + + if (headerAuthUser && headerAuthPassword) { + const headerAuthHash = await hashPassword( + Buffer.from( + `${headerAuthUser}:${headerAuthPassword}` + ).toString("base64") + ); + + await trx.insert(resourceHeaderAuth).values({ + resourceId: newResource.resourceId, + headerAuthHash + }); + } + } + resource = newResource; const [adminRole] = await trx @@ -569,7 +715,8 @@ export async function updateProxyResources( results.push({ proxyResource: resource, - targetsToUpdate + targetsToUpdate, + healthchecksToUpdate }); } @@ -779,6 +926,36 @@ async function syncWhitelistUsers( } } +function checkIfHealthcheckChanged( + existing: TargetHealthCheck | undefined, + incoming: TargetHealthCheck | undefined +) { + if (!existing && incoming) return true; + if (existing && !incoming) return true; + if (!existing || !incoming) return false; + + if (existing.hcEnabled !== incoming.hcEnabled) return true; + if (existing.hcPath !== incoming.hcPath) return true; + if (existing.hcScheme !== incoming.hcScheme) return true; + if (existing.hcMode !== incoming.hcMode) return true; + if (existing.hcHostname !== incoming.hcHostname) return true; + if (existing.hcPort !== incoming.hcPort) return true; + if (existing.hcInterval !== incoming.hcInterval) return true; + if (existing.hcUnhealthyInterval !== incoming.hcUnhealthyInterval) + return true; + if (existing.hcTimeout !== incoming.hcTimeout) return true; + if (existing.hcFollowRedirects !== incoming.hcFollowRedirects) return true; + if (existing.hcMethod !== incoming.hcMethod) return true; + if (existing.hcStatus !== incoming.hcStatus) return true; + if ( + JSON.stringify(existing.hcHeaders) !== + JSON.stringify(incoming.hcHeaders) + ) + return true; + + return false; +} + function checkIfTargetChanged( existing: Target | undefined, incoming: Target | undefined @@ -828,6 +1005,8 @@ async function getDomain( ); } + await createCertificate(domain.domainId, fullDomain, trx); + return domain; } diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 9b3a7a20..02f83f9d 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -5,6 +5,22 @@ export const SiteSchema = z.object({ "docker-socket-enabled": z.boolean().optional().default(true) }); +export const TargetHealthCheckSchema = z.object({ + hostname: z.string(), + port: z.number().int().min(1).max(65535), + enabled: z.boolean().optional().default(true), + path: z.string().optional(), + scheme: z.string().optional(), + mode: z.string().default("http"), + interval: z.number().int().default(30), + unhealthyInterval: z.number().int().default(30), + timeout: z.number().int().default(5), + headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null), + followRedirects: z.boolean().default(true), + method: z.string().default("GET"), + status: z.number().int().optional() +}); + // Schema for individual target within a resource export const TargetSchema = z.object({ site: z.string().optional(), @@ -14,7 +30,11 @@ export const TargetSchema = z.object({ enabled: z.boolean().optional().default(true), "internal-port": z.number().int().min(1).max(65535).optional(), path: z.string().optional(), - "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable() + "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(), + healthcheck: TargetHealthCheckSchema.optional(), + rewritePath: z.string().optional(), + "rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), + priority: z.number().int().min(1).max(1000).optional().default(100) }); export type TargetData = z.infer; @@ -22,6 +42,10 @@ 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(), "sso-enabled": z.boolean().optional().default(false), "sso-roles": z .array(z.string()) @@ -183,7 +207,7 @@ export const ClientResourceSchema = z.object({ "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) + enabled: z.boolean().optional().default(true) }); // Schema for the entire configuration object diff --git a/server/lib/certificates.ts b/server/lib/certificates.ts new file mode 100644 index 00000000..a6c51c96 --- /dev/null +++ b/server/lib/certificates.ts @@ -0,0 +1,13 @@ +export async function getValidCertificatesForDomains(domains: Set): Promise< + Array<{ + id: number; + domain: string; + wildcard: boolean | null; + certFile: string | null; + keyFile: string | null; + expiresAt: number | null; + updatedAt?: number | null; + }> +> { + return []; // stub +} \ No newline at end of file diff --git a/server/lib/colorsSchema.ts b/server/lib/colorsSchema.ts new file mode 100644 index 00000000..0aeb65c3 --- /dev/null +++ b/server/lib/colorsSchema.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +export const colorsSchema = z.object({ + background: z.string().optional(), + foreground: z.string().optional(), + card: z.string().optional(), + "card-foreground": z.string().optional(), + popover: z.string().optional(), + "popover-foreground": z.string().optional(), + primary: z.string().optional(), + "primary-foreground": z.string().optional(), + secondary: z.string().optional(), + "secondary-foreground": z.string().optional(), + muted: z.string().optional(), + "muted-foreground": z.string().optional(), + accent: z.string().optional(), + "accent-foreground": z.string().optional(), + destructive: z.string().optional(), + "destructive-foreground": z.string().optional(), + border: z.string().optional(), + input: z.string().optional(), + ring: z.string().optional(), + radius: z.string().optional(), + "chart-1": z.string().optional(), + "chart-2": z.string().optional(), + "chart-3": z.string().optional(), + "chart-4": z.string().optional(), + "chart-5": z.string().optional() +}); diff --git a/server/lib/config.ts b/server/lib/config.ts index 077cf7b1..6049fa85 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -81,6 +81,10 @@ export class Config { ? "true" : "false"; + if (parsedConfig.server.maxmind_db_path) { + process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; + } + this.rawConfig = parsedConfig; } @@ -106,10 +110,6 @@ export class Config { } return this.rawConfig.domains[domainId]; } - - public isManagedMode() { - return typeof this.rawConfig?.managed === "object"; - } } export const config = new Config(); diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 6c13963a..8ad98167 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.10.2"; +export const APP_VERSION = "1.11.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); @@ -11,3 +11,5 @@ export const APP_PATH = path.join("config"); export const configFilePath1 = path.join(APP_PATH, "config.yml"); export const configFilePath2 = path.join(APP_PATH, "config.yaml"); + +export const privateConfigFilePath1 = path.join(APP_PATH, "privateConfig.yml"); diff --git a/server/lib/corsWithLoginPage.ts b/server/lib/corsWithLoginPage.ts new file mode 100644 index 00000000..43b26264 --- /dev/null +++ b/server/lib/corsWithLoginPage.ts @@ -0,0 +1,92 @@ +import { Request, Response, NextFunction } from "express"; +import cors, { CorsOptions } from "cors"; +import config from "@server/lib/config"; +import logger from "@server/logger"; +import { db, loginPage } from "@server/db"; +import { eq } from "drizzle-orm"; + +async function isValidLoginPageDomain(host: string): Promise { + try { + const [result] = await db + .select() + .from(loginPage) + .where(eq(loginPage.fullDomain, host)) + .limit(1); + + const isValid = !!result; + + return isValid; + } catch (error) { + logger.error("Error checking loginPage domain:", error); + return false; + } +} + +export function corsWithLoginPageSupport(corsConfig: any) { + const options = { + ...(corsConfig?.origins + ? { origin: corsConfig.origins } + : { + origin: (origin: any, callback: any) => { + callback(null, true); + } + }), + ...(corsConfig?.methods && { methods: corsConfig.methods }), + ...(corsConfig?.allowed_headers && { + allowedHeaders: corsConfig.allowed_headers + }), + credentials: !(corsConfig?.credentials === false) + }; + + return async (req: Request, res: Response, next: NextFunction) => { + const originValidatedCorsConfig = { + origin: async ( + origin: string | undefined, + callback: (err: Error | null, allow?: boolean) => void + ) => { + // If no origin (e.g., same-origin request), allow it + + if (!origin) { + return callback(null, true); + } + + const dashboardUrl = config.getRawConfig().app.dashboard_url; + + // If no dashboard_url is configured, allow all origins + if (!dashboardUrl) { + return callback(null, true); + } + + // Check if origin matches dashboard URL + const dashboardHost = new URL(dashboardUrl).host; + const originHost = new URL(origin).host; + + if (originHost === dashboardHost) { + return callback(null, true); + } + + if ( + corsConfig?.origins && + corsConfig.origins.includes(origin) + ) { + return callback(null, true); + } + + // If origin doesn't match dashboard URL, check if it's a valid loginPage domain + const isValidDomain = await isValidLoginPageDomain(originHost); + + if (isValidDomain) { + return callback(null, true); + } + + // Origin is not valid + return callback(null, false); + }, + methods: corsConfig?.methods, + allowedHeaders: corsConfig?.allowed_headers, + credentials: corsConfig?.credentials !== false + } as CorsOptions; + + return cors(originValidatedCorsConfig)(req, res, next); + }; +} diff --git a/server/lib/createUserAccountOrg.ts b/server/lib/createUserAccountOrg.ts new file mode 100644 index 00000000..08e1cb0d --- /dev/null +++ b/server/lib/createUserAccountOrg.ts @@ -0,0 +1,181 @@ +import { isValidCIDR } from "@server/lib/validators"; +import { getNextAvailableOrgSubnet } from "@server/lib/ip"; +import { + actions, + apiKeyOrg, + apiKeys, + db, + domains, + Org, + orgDomains, + orgs, + roleActions, + roles, + userOrgs +} from "@server/db"; +import { eq } from "drizzle-orm"; +import { defaultRoleAllowedActions } from "@server/routers/role"; + +export async function createUserAccountOrg( + userId: string, + userEmail: string +): Promise<{ + success: boolean; + org?: { + orgId: string; + name: string; + subnet: string; + }; + error?: string; +}> { + // const subnet = await getNextAvailableOrgSubnet(); + const orgId = "org_" + userId; + const name = `${userEmail}'s Organization`; + + // if (!isValidCIDR(subnet)) { + // return { + // success: false, + // error: "Invalid subnet format. Please provide a valid CIDR notation." + // }; + // } + + // // make sure the subnet is unique + // const subnetExists = await db + // .select() + // .from(orgs) + // .where(eq(orgs.subnet, subnet)) + // .limit(1); + + // if (subnetExists.length > 0) { + // return { success: false, error: `Subnet ${subnet} already exists` }; + // } + + // make sure the orgId is unique + const orgExists = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + if (orgExists.length > 0) { + return { + success: false, + error: `Organization with ID ${orgId} already exists` + }; + } + + let error = ""; + let org: Org | null = null; + + await db.transaction(async (trx) => { + const allDomains = await trx + .select() + .from(domains) + .where(eq(domains.configManaged, true)); + + const newOrg = await trx + .insert(orgs) + .values({ + orgId, + name, + // subnet + subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs? + createdAt: new Date().toISOString() + }) + .returning(); + + if (newOrg.length === 0) { + error = "Failed to create organization"; + trx.rollback(); + return; + } + + org = newOrg[0]; + + // Create admin role within the same transaction + const [insertedRole] = await trx + .insert(roles) + .values({ + orgId: newOrg[0].orgId, + isAdmin: true, + name: "Admin", + description: "Admin role with the most permissions" + }) + .returning({ roleId: roles.roleId }); + + if (!insertedRole || !insertedRole.roleId) { + error = "Failed to create Admin role"; + trx.rollback(); + return; + } + + const roleId = insertedRole.roleId; + + // Get all actions and create role actions + const actionIds = await trx.select().from(actions).execute(); + + if (actionIds.length > 0) { + await trx.insert(roleActions).values( + actionIds.map((action) => ({ + roleId, + actionId: action.actionId, + orgId: newOrg[0].orgId + })) + ); + } + + if (allDomains.length) { + await trx.insert(orgDomains).values( + allDomains.map((domain) => ({ + orgId: newOrg[0].orgId, + domainId: domain.domainId + })) + ); + } + + await trx.insert(userOrgs).values({ + userId, + orgId: newOrg[0].orgId, + roleId: roleId, + isOwner: true + }); + + const memberRole = await trx + .insert(roles) + .values({ + name: "Member", + description: "Members can only view resources", + orgId + }) + .returning(); + + await trx.insert(roleActions).values( + defaultRoleAllowedActions.map((action) => ({ + roleId: memberRole[0].roleId, + actionId: action, + orgId + })) + ); + }); + + if (!org) { + return { success: false, error: "Failed to create org" }; + } + + if (error) { + return { + success: false, + error: `Failed to create org: ${error}` + }; + } + + return { + org: { + orgId, + name, + // subnet + subnet: "100.90.128.0/24" + }, + success: true + }; +} diff --git a/server/lib/encryption.ts b/server/lib/encryption.ts new file mode 100644 index 00000000..7959fa4b --- /dev/null +++ b/server/lib/encryption.ts @@ -0,0 +1,39 @@ +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; +} + +// 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; +} + +// openssl rand -hex 32 > config/encryption.key \ No newline at end of file diff --git a/server/lib/exitNodeComms.ts b/server/lib/exitNodes/exitNodeComms.ts similarity index 100% rename from server/lib/exitNodeComms.ts rename to server/lib/exitNodes/exitNodeComms.ts diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts index 7b571682..bb269710 100644 --- a/server/lib/exitNodes/exitNodes.ts +++ b/server/lib/exitNodes/exitNodes.ts @@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess( return { hasAccess: true, exitNode }; } -export async function listExitNodes(orgId: string, filterOnline = false) { +export async function listExitNodes( + orgId: string, + filterOnline = false, + noCloud = false +) { // TODO: pick which nodes to send and ping better than just all of them that are not remote const allExitNodes = await db .select({ @@ -57,4 +61,18 @@ export function selectBestExitNode( export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { return false; -} \ No newline at end of file +} + +export async function resolveExitNodes( + hostname: string, + publicKey: string +): Promise< + { + endpoint: string; + publicKey: string; + orgId: string; + }[] +> { + // OSS version: simple implementation that returns empty array + return []; +} diff --git a/server/lib/exitNodes/getCurrentExitNodeId.ts b/server/lib/exitNodes/getCurrentExitNodeId.ts new file mode 100644 index 00000000..d895ce42 --- /dev/null +++ b/server/lib/exitNodes/getCurrentExitNodeId.ts @@ -0,0 +1,33 @@ +import { eq } from "drizzle-orm"; +import { db, exitNodes } from "@server/db"; +import config from "@server/lib/config"; + +let currentExitNodeId: number; // we really only need to look this up once per instance +export async function getCurrentExitNodeId(): Promise { + if (!currentExitNodeId) { + if (config.getRawConfig().gerbil.exit_node_name) { + const exitNodeName = config.getRawConfig().gerbil.exit_node_name!; + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .where(eq(exitNodes.name, exitNodeName)); + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; + } + } else { + const [exitNode] = await db + .select({ + exitNodeId: exitNodes.exitNodeId + }) + .from(exitNodes) + .limit(1); + + if (exitNode) { + currentExitNodeId = exitNode.exitNodeId; + } + } + } + return currentExitNodeId; +} \ No newline at end of file diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts index 8889bc35..ba30ccc2 100644 --- a/server/lib/exitNodes/index.ts +++ b/server/lib/exitNodes/index.ts @@ -1,2 +1,4 @@ export * from "./exitNodes"; -export * from "./shared"; \ No newline at end of file +export * from "./exitNodeComms"; +export * from "./subnet"; +export * from "./getCurrentExitNodeId"; \ No newline at end of file diff --git a/server/lib/exitNodes/shared.ts b/server/lib/exitNodes/subnet.ts similarity index 100% rename from server/lib/exitNodes/shared.ts rename to server/lib/exitNodes/subnet.ts diff --git a/server/lib/geoip.ts b/server/lib/geoip.ts index 042e53c9..ac739fa3 100644 --- a/server/lib/geoip.ts +++ b/server/lib/geoip.ts @@ -1,32 +1,33 @@ -import axios from "axios"; -import config from "./config"; -import { tokenManager } from "./tokenManager"; import logger from "@server/logger"; +import { maxmindLookup } from "@server/db/maxmind"; export async function getCountryCodeForIp( ip: string ): Promise { try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`, - await tokenManager.getAuthHeader() + if (!maxmindLookup) { + logger.warn( + "MaxMind DB path not configured, cannot perform GeoIP lookup" + ); + return; + } + + const result = maxmindLookup.get(ip); + + if (!result || !result.country) { + return; + } + + const { country } = result; + + logger.debug( + `GeoIP lookup successful for IP ${ip}: ${country.iso_code}` ); - return response.data.data.countryCode; + return country.iso_code; } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error fetching config in verify session:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching config in verify session:", error); - } + logger.error("Error fetching config in verify session:", error); } return; -} +} \ No newline at end of file diff --git a/server/lib/idp/generateRedirectUrl.ts b/server/lib/idp/generateRedirectUrl.ts index 4eea973e..077ac6f6 100644 --- a/server/lib/idp/generateRedirectUrl.ts +++ b/server/lib/idp/generateRedirectUrl.ts @@ -1,8 +1,48 @@ +import { db, loginPage, loginPageOrg } from "@server/db"; import config from "@server/lib/config"; +import { eq } from "drizzle-orm"; + +export async function generateOidcRedirectUrl( + idpId: number, + orgId?: string, + loginPageId?: number +): Promise { + let baseUrl: string | undefined; + + const secure = config.getRawConfig().app.dashboard_url?.startsWith("https"); + const method = secure ? "https" : "http"; + + if (loginPageId) { + const [res] = await db + .select() + .from(loginPage) + .where(eq(loginPage.loginPageId, loginPageId)) + .limit(1); + + if (res && res.fullDomain) { + baseUrl = `${method}://${res.fullDomain}`; + } + } else if (orgId) { + const [res] = await db + .select() + .from(loginPageOrg) + .where(eq(loginPageOrg.orgId, orgId)) + .innerJoin( + loginPage, + eq(loginPage.loginPageId, loginPageOrg.loginPageId) + ) + .limit(1); + + if (res?.loginPage && res.loginPage.domainId && res.loginPage.fullDomain) { + baseUrl = `${method}://${res.loginPage.fullDomain}`; + } + } + + if (!baseUrl) { + baseUrl = config.getRawConfig().app.dashboard_url!; + } -export function generateOidcRedirectUrl(idpId: number) { - const dashboardUrl = config.getRawConfig().app.dashboard_url; const redirectPath = `/auth/idp/${idpId}/oidc/callback`; - const redirectUrl = new URL(redirectPath, dashboardUrl).toString(); + const redirectUrl = new URL(redirectPath, baseUrl!).toString(); return redirectUrl; } diff --git a/server/lib/index.ts b/server/lib/index.ts deleted file mode 100644 index db1a73da..00000000 --- a/server/lib/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./response"; -export { tokenManager, TokenManager } from "./tokenManager"; -export * from "./geoip"; diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index ae2cc120..6d57399d 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -12,33 +12,27 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { export const configSchema = z .object({ - app: z.object({ - dashboard_url: z - .string() - .url() - .pipe(z.string().url()) - .transform((url) => url.toLowerCase()) - .optional(), - log_level: z - .enum(["debug", "info", "warn", "error"]) - .optional() - .default("info"), - save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false), - }).optional().default({ - log_level: "info", - save_logs: false, - log_failed_attempts: false, - }), - managed: z + app: z .object({ - name: z.string().optional(), - id: z.string().optional(), - secret: z.string().optional(), - endpoint: z.string().optional().default("https://pangolin.fossorial.io"), - redirect_endpoint: z.string().optional() + dashboard_url: z + .string() + .url() + .pipe(z.string().url()) + .transform((url) => url.toLowerCase()) + .optional(), + log_level: z + .enum(["debug", "info", "warn", "error"]) + .optional() + .default("info"), + save_logs: z.boolean().optional().default(false), + log_failed_attempts: z.boolean().optional().default(false), }) - .optional(), + .optional() + .default({ + log_level: "info", + save_logs: false, + log_failed_attempts: false + }), domains: z .record( z.string(), @@ -52,103 +46,130 @@ export const configSchema = z }) ) .optional(), - server: z.object({ - integration_port: portSchema - .optional() - .default(3004) - .transform(stoi) - .pipe(portSchema.optional()), - external_port: portSchema - .optional() - .default(3000) - .transform(stoi) - .pipe(portSchema), - internal_port: portSchema - .optional() - .default(3001) - .transform(stoi) - .pipe(portSchema), - next_port: portSchema - .optional() - .default(3002) - .transform(stoi) - .pipe(portSchema), - internal_hostname: z - .string() - .optional() - .default("pangolin") - .transform((url) => url.toLowerCase()), - session_cookie_name: z - .string() - .optional() - .default("p_session_token"), - resource_access_token_param: z - .string() - .optional() - .default("p_token"), - resource_access_token_headers: z - .object({ - id: z.string().optional().default("P-Access-Token-Id"), - token: z.string().optional().default("P-Access-Token") - }) - .optional() - .default({}), - resource_session_request_param: z - .string() - .optional() - .default("resource_session_request_param"), - dashboard_session_length_hours: z - .number() - .positive() - .gt(0) - .optional() - .default(720), - resource_session_length_hours: z - .number() - .positive() - .gt(0) - .optional() - .default(720), - cors: z - .object({ - origins: z.array(z.string()).optional(), - methods: z.array(z.string()).optional(), - allowed_headers: z.array(z.string()).optional(), - credentials: z.boolean().optional() - }) - .optional(), - trust_proxy: z.number().int().gte(0).optional().default(1), - secret: z - .string() - .pipe(z.string().min(8)) - .optional() - }).optional().default({ - integration_port: 3003, - external_port: 3000, - internal_port: 3001, - next_port: 3002, - internal_hostname: "pangolin", - session_cookie_name: "p_session_token", - resource_access_token_param: "p_token", - resource_access_token_headers: { - id: "P-Access-Token-Id", - token: "P-Access-Token" - }, - resource_session_request_param: "resource_session_request_param", - dashboard_session_length_hours: 720, - resource_session_length_hours: 720, - trust_proxy: 1 - }), + server: z + .object({ + integration_port: portSchema + .optional() + .default(3003) + .transform(stoi) + .pipe(portSchema.optional()), + external_port: portSchema + .optional() + .default(3000) + .transform(stoi) + .pipe(portSchema), + internal_port: portSchema + .optional() + .default(3001) + .transform(stoi) + .pipe(portSchema), + next_port: portSchema + .optional() + .default(3002) + .transform(stoi) + .pipe(portSchema), + internal_hostname: z + .string() + .optional() + .default("pangolin") + .transform((url) => url.toLowerCase()), + session_cookie_name: z + .string() + .optional() + .default("p_session_token"), + resource_access_token_param: z + .string() + .optional() + .default("p_token"), + resource_access_token_headers: z + .object({ + id: z.string().optional().default("P-Access-Token-Id"), + token: z.string().optional().default("P-Access-Token") + }) + .optional() + .default({}), + resource_session_request_param: z + .string() + .optional() + .default("resource_session_request_param"), + dashboard_session_length_hours: z + .number() + .positive() + .gt(0) + .optional() + .default(720), + resource_session_length_hours: z + .number() + .positive() + .gt(0) + .optional() + .default(720), + cors: z + .object({ + origins: z.array(z.string()).optional(), + methods: z.array(z.string()).optional(), + allowed_headers: z.array(z.string()).optional(), + credentials: z.boolean().optional() + }) + .optional(), + trust_proxy: z.number().int().gte(0).optional().default(1), + secret: z.string().pipe(z.string().min(8)).optional(), + maxmind_db_path: z.string().optional() + }) + .optional() + .default({ + integration_port: 3003, + external_port: 3000, + internal_port: 3001, + next_port: 3002, + internal_hostname: "pangolin", + session_cookie_name: "p_session_token", + resource_access_token_param: "p_token", + resource_access_token_headers: { + id: "P-Access-Token-Id", + token: "P-Access-Token" + }, + resource_session_request_param: + "resource_session_request_param", + dashboard_session_length_hours: 720, + resource_session_length_hours: 720, + trust_proxy: 1 + }), postgres: z .object({ - connection_string: z.string(), + connection_string: z.string().optional(), replicas: z .array( z.object({ connection_string: z.string() }) ) + .optional(), + pool: z + .object({ + max_connections: z + .number() + .positive() + .optional() + .default(20), + max_replica_connections: z + .number() + .positive() + .optional() + .default(10), + idle_timeout_ms: z + .number() + .positive() + .optional() + .default(30000), + connection_timeout_ms: z + .number() + .positive() + .optional() + .default(5000) + }) .optional() + .default({}) }) .optional(), traefik: z @@ -169,7 +190,10 @@ export const configSchema = z .optional() .default("/var/dynamic/router_config.yml"), static_domains: z.array(z.string()).optional().default([]), - site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]), + site_types: z + .array(z.string()) + .optional() + .default(["newt", "wireguard", "local"]), allow_raw_resources: z.boolean().optional().default(true), file_mode: z.boolean().optional().default(false) }) @@ -296,10 +320,7 @@ export const configSchema = z if (data.flags?.disable_config_managed_domains) { return true; } - // If hybrid is defined, domains are not required - if (data.managed) { - return true; - } + if (keys.length === 0) { return false; } @@ -311,15 +332,14 @@ export const configSchema = z ) .refine( (data) => { - // If hybrid is defined, server secret is not required - if (data.managed) { - return true; - } // If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env if (data.server?.secret === undefined) { data.server.secret = process.env.SERVER_SECRET; } - return data.server?.secret !== undefined && data.server.secret.length > 0; + return ( + data.server?.secret !== undefined && + data.server.secret.length > 0 + ); }, { message: "Server secret must be defined" @@ -327,12 +347,11 @@ export const configSchema = z ) .refine( (data) => { - // If hybrid is defined, dashboard_url is not required - if (data.managed) { - return true; - } // If hybrid is not defined, dashboard_url must be defined - return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0; + return ( + data.app.dashboard_url !== undefined && + data.app.dashboard_url.length > 0 + ); }, { message: "Dashboard URL must be defined" diff --git a/server/lib/remoteCertificates/certificates.ts b/server/lib/remoteCertificates/certificates.ts deleted file mode 100644 index 9a4ce001..00000000 --- a/server/lib/remoteCertificates/certificates.ts +++ /dev/null @@ -1,80 +0,0 @@ -import axios from "axios"; -import { tokenManager } from "../tokenManager"; -import logger from "@server/logger"; -import config from "../config"; - -/** - * Get valid certificates for the specified domains - */ -export async function getValidCertificatesForDomainsHybrid(domains: Set): Promise< - Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: Date | null; - updatedAt?: Date | null; - }> -> { - if (domains.size === 0) { - return []; - } - - const domainArray = Array.from(domains); - - try { - const response = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`, - { - params: { - domains: domainArray - }, - headers: (await tokenManager.getAuthHeader()).headers - } - ); - - if (response.status !== 200) { - logger.error( - `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`, - { responseData: response.data, domains: domainArray } - ); - return []; - } - - // logger.debug( - // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains` - // ); - - return response.data.data; - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error getting certificates:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error getting certificates:", error); - } - return []; - } -} - -export async function getValidCertificatesForDomains(domains: Set): Promise< - Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: Date | null; - updatedAt?: Date | null; - }> -> { - return []; // stub -} \ No newline at end of file diff --git a/server/lib/remoteCertificates/index.ts b/server/lib/remoteCertificates/index.ts deleted file mode 100644 index 53051b6c..00000000 --- a/server/lib/remoteCertificates/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./certificates"; \ No newline at end of file diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts deleted file mode 100644 index c9016071..00000000 --- a/server/lib/remoteProxy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { Router } from "express"; -import axios from "axios"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import config from "@server/lib/config"; -import { tokenManager } from "./tokenManager"; - -/** - * Proxy function that forwards requests to the remote cloud server - */ - -export const proxyToRemote = async ( - req: Request, - res: Response, - next: NextFunction, - endpoint: string -): Promise => { - try { - const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`; - - logger.debug(`Proxying request to remote server: ${remoteUrl}`); - - // Forward the request to the remote server - const response = await axios({ - method: req.method as any, - url: remoteUrl, - data: req.body, - headers: { - 'Content-Type': 'application/json', - ...(await tokenManager.getAuthHeader()).headers - }, - params: req.query, - timeout: 30000, // 30 second timeout - validateStatus: () => true // Don't throw on non-2xx status codes - }); - - logger.debug(`Proxy response: ${JSON.stringify(response.data)}`); - - // Forward the response status and data - return res.status(response.status).json(response.data); - - } catch (error) { - logger.error("Error proxying request to remote server:", error); - - if (axios.isAxiosError(error)) { - if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { - return next( - createHttpError( - HttpCode.SERVICE_UNAVAILABLE, - "Remote server is unavailable" - ) - ); - } - if (error.code === 'ECONNABORTED') { - return next( - createHttpError( - HttpCode.REQUEST_TIMEOUT, - "Request to remote server timed out" - ) - ); - } - } - - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error communicating with remote server" - ) - ); - } -}; \ No newline at end of file diff --git a/server/lib/resend.ts b/server/lib/resend.ts new file mode 100644 index 00000000..7dd130c8 --- /dev/null +++ b/server/lib/resend.ts @@ -0,0 +1,15 @@ +export enum AudienceIds { + General = "", + Subscribed = "", + Churned = "" +} + +let resend; +export default resend; + +export async function moveEmailToAudience( + email: string, + audienceId: AudienceIds +) { + return; +} \ No newline at end of file diff --git a/server/lib/schemas.ts b/server/lib/schemas.ts index 0888ff31..5e2bd400 100644 --- a/server/lib/schemas.ts +++ b/server/lib/schemas.ts @@ -17,3 +17,12 @@ export const tlsNameSchema = z ) .transform((val) => val.toLowerCase()); +export const privateNamespaceSubdomainSchema = z + .string() + .regex( + /^[a-zA-Z0-9-]+$/, + "Namespace subdomain can only contain letters, numbers, and hyphens" + ) + .min(1, "Namespace subdomain must be at least 1 character long") + .max(32, "Namespace subdomain must be at most 32 characters long") + .transform((val) => val.toLowerCase()); diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts deleted file mode 100644 index 2e0e1118..00000000 --- a/server/lib/tokenManager.ts +++ /dev/null @@ -1,274 +0,0 @@ -import axios from "axios"; -import config from "@server/lib/config"; -import logger from "@server/logger"; - -export interface TokenResponse { - success: boolean; - message?: string; - data: { - token: string; - }; -} - -/** - * Token Manager - Handles automatic token refresh for hybrid server authentication - * - * Usage throughout the application: - * ```typescript - * import { tokenManager } from "@server/lib/tokenManager"; - * - * // Get the current valid token - * const token = await tokenManager.getToken(); - * - * // Force refresh if needed - * await tokenManager.refreshToken(); - * ``` - * - * The token manager automatically refreshes tokens every 24 hours by default - * and is started once in the privateHybridServer.ts file. - */ - -export class TokenManager { - private token: string | null = null; - private refreshInterval: NodeJS.Timeout | null = null; - private isRefreshing: boolean = false; - private refreshIntervalMs: number; - private retryInterval: NodeJS.Timeout | null = null; - private retryIntervalMs: number; - private tokenAvailablePromise: Promise | null = null; - private tokenAvailableResolve: (() => void) | null = null; - - constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) { - // Default to 24 hours for refresh, 5 seconds for retry - this.refreshIntervalMs = refreshIntervalMs; - this.retryIntervalMs = retryIntervalMs; - this.setupTokenAvailablePromise(); - } - - /** - * Set up promise that resolves when token becomes available - */ - private setupTokenAvailablePromise(): void { - this.tokenAvailablePromise = new Promise((resolve) => { - this.tokenAvailableResolve = resolve; - }); - } - - /** - * Resolve the token available promise - */ - private resolveTokenAvailable(): void { - if (this.tokenAvailableResolve) { - this.tokenAvailableResolve(); - this.tokenAvailableResolve = null; - } - } - - /** - * Start the token manager - gets initial token and sets up refresh interval - * If initial token fetch fails, keeps retrying every few seconds until successful - */ - async start(): Promise { - logger.info("Starting token manager..."); - - try { - await this.refreshToken(); - this.setupRefreshInterval(); - this.resolveTokenAvailable(); - logger.info("Token manager started successfully"); - } catch (error) { - logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error); - this.setupRetryInterval(); - } - } - - /** - * Set up retry interval for initial token acquisition - */ - private setupRetryInterval(): void { - if (this.retryInterval) { - clearInterval(this.retryInterval); - } - - this.retryInterval = setInterval(async () => { - try { - logger.debug("Retrying initial token acquisition"); - await this.refreshToken(); - this.setupRefreshInterval(); - this.clearRetryInterval(); - this.resolveTokenAvailable(); - logger.info("Token manager started successfully after retry"); - } catch (error) { - logger.debug("Token acquisition retry failed, will try again"); - } - }, this.retryIntervalMs); - } - - /** - * Clear retry interval - */ - private clearRetryInterval(): void { - if (this.retryInterval) { - clearInterval(this.retryInterval); - this.retryInterval = null; - } - } - - /** - * Stop the token manager and clear all intervals - */ - stop(): void { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - this.clearRetryInterval(); - logger.info("Token manager stopped"); - } - - /** - * Get the current valid token - */ - - // TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT - async getToken(): Promise { - // If we don't have a token yet, wait for it to become available - if (!this.token && this.tokenAvailablePromise) { - await this.tokenAvailablePromise; - } - - if (!this.token) { - if (this.isRefreshing) { - // Wait for current refresh to complete - await this.waitForRefresh(); - } else { - throw new Error("No valid token available"); - } - } - - if (!this.token) { - throw new Error("No valid token available"); - } - - return this.token; - } - - async getAuthHeader() { - return { - headers: { - Authorization: `Bearer ${await this.getToken()}`, - "X-CSRF-Token": "x-csrf-protection", - } - }; - } - - /** - * Force refresh the token - */ - async refreshToken(): Promise { - if (this.isRefreshing) { - await this.waitForRefresh(); - return; - } - - this.isRefreshing = true; - - try { - const hybridConfig = config.getRawConfig().managed; - - if ( - !hybridConfig?.id || - !hybridConfig?.secret || - !hybridConfig?.endpoint - ) { - throw new Error("Hybrid configuration is not defined"); - } - - const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`; - - const tokenData = { - remoteExitNodeId: hybridConfig.id, - secret: hybridConfig.secret - }; - - logger.debug("Requesting new token from server"); - - const response = await axios.post( - tokenEndpoint, - tokenData, - { - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": "x-csrf-protection" - }, - timeout: 10000 // 10 second timeout - } - ); - - if (!response.data.success) { - throw new Error( - `Failed to get token: ${response.data.message}` - ); - } - - if (!response.data.data.token) { - throw new Error("Received empty token from server"); - } - - this.token = response.data.data.token; - logger.debug("Token refreshed successfully"); - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error("Error updating proxy mapping:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error updating proxy mapping:", error); - } - - throw new Error("Failed to refresh token"); - } finally { - this.isRefreshing = false; - } - } - - /** - * Set up automatic token refresh interval - */ - private setupRefreshInterval(): void { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - } - - this.refreshInterval = setInterval(async () => { - try { - logger.debug("Auto-refreshing token"); - await this.refreshToken(); - } catch (error) { - logger.error("Failed to auto-refresh token:", error); - } - }, this.refreshIntervalMs); - } - - /** - * Wait for current refresh operation to complete - */ - private async waitForRefresh(): Promise { - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (!this.isRefreshing) { - clearInterval(checkInterval); - resolve(); - } - }, 100); - }); - } -} - -// Export a singleton instance for use throughout the application -export const tokenManager = new TokenManager(); diff --git a/server/lib/traefikConfig.ts b/server/lib/traefik/TraefikConfigManager.ts similarity index 84% rename from server/lib/traefikConfig.ts rename to server/lib/traefik/TraefikConfigManager.ts index 8b133419..030ad4ab 100644 --- a/server/lib/traefikConfig.ts +++ b/server/lib/traefik/TraefikConfigManager.ts @@ -6,16 +6,9 @@ import * as yaml from "js-yaml"; import axios from "axios"; import { db, exitNodes } from "@server/db"; import { eq } from "drizzle-orm"; -import { tokenManager } from "./tokenManager"; -import { - getCurrentExitNodeId, - getTraefikConfig -} from "@server/routers/traefik"; -import { - getValidCertificatesForDomains, - getValidCertificatesForDomainsHybrid -} from "./remoteCertificates"; -import { sendToExitNode } from "./exitNodeComms"; +import { getCurrentExitNodeId } from "@server/lib/exitNodes"; +import { getTraefikConfig } from "@server/lib/traefik"; +import { sendToExitNode } from "@server/lib/exitNodes"; export class TraefikConfigManager { private intervalId: NodeJS.Timeout | null = null; @@ -28,8 +21,8 @@ export class TraefikConfigManager { string, { exists: boolean; - lastModified: Date | null; - expiresAt: Date | null; + lastModified: number | null; + expiresAt: number | null; wildcard: boolean | null; } >(); @@ -115,8 +108,8 @@ export class TraefikConfigManager { string, { exists: boolean; - lastModified: Date | null; - expiresAt: Date | null; + lastModified: number | null; + expiresAt: number | null; wildcard: boolean; } > @@ -217,7 +210,12 @@ export class TraefikConfigManager { // Filter out domains covered by wildcard certificates const domainsNeedingCerts = new Set(); for (const domain of currentDomains) { - if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) { + if ( + !isDomainCoveredByWildcard( + domain, + this.lastLocalCertificateState + ) + ) { domainsNeedingCerts.add(domain); } } @@ -225,7 +223,12 @@ export class TraefikConfigManager { // Fetch if domains needing certificates have changed const lastDomainsNeedingCerts = new Set(); for (const domain of this.lastKnownDomains) { - if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) { + if ( + !isDomainCoveredByWildcard( + domain, + this.lastLocalCertificateState + ) + ) { lastDomainsNeedingCerts.add(domain); } } @@ -255,7 +258,7 @@ export class TraefikConfigManager { // Check if certificate is expiring soon (within 30 days) if (localState.expiresAt) { const daysUntilExpiry = - (localState.expiresAt.getTime() - Date.now()) / + (localState.expiresAt - Math.floor(Date.now() / 1000)) / (1000 * 60 * 60 * 24); if (daysUntilExpiry < 30) { logger.info( @@ -276,7 +279,7 @@ export class TraefikConfigManager { public async HandleTraefikConfig(): Promise { try { // Get all active domains for this exit node via HTTP call - const getTraefikConfig = await this.getTraefikConfig(); + const getTraefikConfig = await this.internalGetTraefikConfig(); if (!getTraefikConfig) { logger.error( @@ -304,88 +307,6 @@ export class TraefikConfigManager { this.lastActiveDomains = new Set(domains); } - // Scan current local certificate state - this.lastLocalCertificateState = - await this.scanLocalCertificateState(); - - // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) - let validCertificates: Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: Date | null; - updatedAt?: Date | null; - }> = []; - - if (this.shouldFetchCertificates(domains)) { - // Filter out domains that are already covered by wildcard certificates - const domainsToFetch = new Set(); - for (const domain of domains) { - if (!isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) { - domainsToFetch.add(domain); - } else { - logger.debug( - `Domain ${domain} is covered by existing wildcard certificate, skipping fetch` - ); - } - } - - if (domainsToFetch.size > 0) { - // Get valid certificates for domains not covered by wildcards - if (config.isManagedMode()) { - validCertificates = - await getValidCertificatesForDomainsHybrid( - domainsToFetch - ); - } else { - validCertificates = - await getValidCertificatesForDomains( - domainsToFetch - ); - } - this.lastCertificateFetch = new Date(); - this.lastKnownDomains = new Set(domains); - - logger.info( - `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` - ); - - // Download and decrypt new certificates - await this.processValidCertificates(validCertificates); - } else { - logger.info( - "All domains are covered by existing wildcard certificates, no fetch needed" - ); - this.lastCertificateFetch = new Date(); - this.lastKnownDomains = new Set(domains); - } - - // Always ensure all existing certificates (including wildcards) are in the config - await this.updateDynamicConfigFromLocalCerts(domains); - } else { - const timeSinceLastFetch = this.lastCertificateFetch - ? Math.round( - (Date.now() - this.lastCertificateFetch.getTime()) / - (1000 * 60) - ) - : 0; - - // logger.debug( - // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` - // ); - - // Still need to ensure config is up to date with existing certificates - await this.updateDynamicConfigFromLocalCerts(domains); - } - - // Clean up certificates for domains no longer in use - await this.cleanupUnusedCertificates(domains); - - // wait 1 second for traefik to pick up the new certificates - await new Promise((resolve) => setTimeout(resolve, 500)); - // Write traefik config as YAML to a second dynamic config file if changed await this.writeTraefikDynamicConfig(traefikConfig); @@ -428,34 +349,21 @@ export class TraefikConfigManager { /** * Get all domains currently in use from traefik config API */ - private async getTraefikConfig(): Promise<{ + private async internalGetTraefikConfig(): Promise<{ domains: Set; traefikConfig: any; } | null> { let traefikConfig; try { - if (config.isManagedMode()) { - const resp = await axios.get( - `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`, - await tokenManager.getAuthHeader() - ); - - if (resp.status !== 200) { - logger.error( - `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, - { responseData: resp.data } - ); - return null; - } - - traefikConfig = resp.data.data; - } else { - const currentExitNode = await getCurrentExitNodeId(); - traefikConfig = await getTraefikConfig( - currentExitNode, - config.getRawConfig().traefik.site_types - ); - } + const currentExitNode = await getCurrentExitNodeId(); + // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`); + traefikConfig = await getTraefikConfig( + // this is called by the local exit node to get its own config + currentExitNode, + config.getRawConfig().traefik.site_types, + true, // filter out the namespace domains in open source + false // generate the login pages on the cloud and hybrid + ); const domains = new Set(); @@ -621,7 +529,8 @@ export class TraefikConfigManager { } // If no exact match, check for wildcard certificates that cover this domain - for (const [certDomain, certState] of this.lastLocalCertificateState) { + for (const [certDomain, certState] of this + .lastLocalCertificateState) { if (certState.exists && certState.wildcard) { // Check if this wildcard certificate covers the domain if (domain.endsWith("." + certDomain)) { @@ -671,8 +580,8 @@ export class TraefikConfigManager { wildcard: boolean | null; certFile: string | null; keyFile: string | null; - expiresAt: Date | null; - updatedAt?: Date | null; + expiresAt: number | null; + updatedAt?: number | null; }> ): Promise { const dynamicConfigPath = @@ -699,7 +608,12 @@ export class TraefikConfigManager { for (const cert of validCertificates) { try { - if (!cert.certFile || !cert.keyFile) { + if ( + !cert.certFile || + !cert.keyFile || + cert.certFile.length === 0 || + cert.keyFile.length === 0 + ) { logger.warn( `Certificate for domain ${cert.domain} is missing cert or key file` ); @@ -758,7 +672,7 @@ export class TraefikConfigManager { // Update local state tracking this.lastLocalCertificateState.set(cert.domain, { exists: true, - lastModified: new Date(), + lastModified: Math.floor(Date.now() / 1000), expiresAt: cert.expiresAt, wildcard: cert.wildcard }); @@ -800,8 +714,8 @@ export class TraefikConfigManager { cert: { id: number; domain: string; - expiresAt: Date | null; - updatedAt?: Date | null; + expiresAt: number | null; + updatedAt?: number | null; }, certPath: string, keyPath: string, @@ -818,12 +732,14 @@ export class TraefikConfigManager { } // Read last update time from .last_update file - let lastUpdateTime: Date | null = null; + let lastUpdateTime: number | null = null; try { const lastUpdateStr = fs .readFileSync(lastUpdatePath, "utf8") .trim(); - lastUpdateTime = new Date(lastUpdateStr); + lastUpdateTime = Math.floor( + new Date(lastUpdateStr).getTime() / 1000 + ); } catch { lastUpdateTime = null; } @@ -1004,7 +920,12 @@ export class TraefikConfigManager { // Find domains covered by wildcards for (const domain of this.activeDomains) { - if (isDomainCoveredByWildcard(domain, this.lastLocalCertificateState)) { + if ( + isDomainCoveredByWildcard( + domain, + this.lastLocalCertificateState + ) + ) { domainsCoveredByWildcards.push(domain); } } @@ -1025,7 +946,13 @@ export class TraefikConfigManager { /** * Check if a domain is covered by existing wildcard certificates */ -export function isDomainCoveredByWildcard(domain: string, lastLocalCertificateState: Map): boolean { +export function isDomainCoveredByWildcard( + domain: string, + lastLocalCertificateState: Map< + string, + { exists: boolean; wildcard: boolean | null } + > +): boolean { for (const [certDomain, state] of lastLocalCertificateState) { if (state.exists && state.wildcard) { // If stored as example.com but is wildcard, check subdomains diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts similarity index 72% rename from server/routers/traefik/getTraefikConfig.ts rename to server/lib/traefik/getTraefikConfig.ts index 5101de84..cf4d5d42 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -1,98 +1,29 @@ -import { Request, Response } from "express"; -import { db, exitNodes } from "@server/db"; -import { and, eq, inArray, or, isNull, ne, isNotNull } from "drizzle-orm"; +import { db, targetHealthCheck } from "@server/db"; +import { + and, + eq, + inArray, + or, + isNull, + ne, + isNotNull, + desc, + sql +} from "drizzle-orm"; import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; -import { orgs, resources, sites, Target, targets } from "@server/db"; -import { build } from "@server/build"; +import { resources, sites, Target, targets } from "@server/db"; +import createPathRewriteMiddleware from "./middleware"; +import { sanitize, validatePathRewriteConfig } from "./utils"; -let currentExitNodeId: number; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; -export async function getCurrentExitNodeId(): Promise { - if (!currentExitNodeId) { - if (config.getRawConfig().gerbil.exit_node_name) { - const exitNodeName = config.getRawConfig().gerbil.exit_node_name!; - const [exitNode] = await db - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .where(eq(exitNodes.name, exitNodeName)); - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } else { - const [exitNode] = await db - .select({ - exitNodeId: exitNodes.exitNodeId - }) - .from(exitNodes) - .limit(1); - - if (exitNode) { - currentExitNodeId = exitNode.exitNodeId; - } - } - } - return currentExitNodeId; -} - -export async function traefikConfigProvider( - _: Request, - res: Response -): Promise { - try { - // First query to get resources with site and org info - // Get the current exit node name from config - await getCurrentExitNodeId(); - - const traefikConfig = await getTraefikConfig( - currentExitNodeId, - config.getRawConfig().traefik.site_types - ); - - if (traefikConfig?.http?.middlewares) { - // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING - traefikConfig.http.middlewares[badgerMiddlewareName] = { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server.internal_hostname - }:${config.getRawConfig().server.internal_port}` - ).href, - userSessionCookieName: - config.getRawConfig().server.session_cookie_name, - - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, - - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param - } - } - }; - } - - return res.status(HttpCode.OK).json(traefikConfig); - } catch (e) { - logger.error(`Failed to build Traefik config: ${e}`); - return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({ - error: "Failed to build Traefik config" - }); - } -} - export async function getTraefikConfig( exitNodeId: number, - siteTypes: string[] + siteTypes: string[], + filterOutNamespaceDomains = false, + generateLoginPageRouters = false ): Promise { // Define extended target type with site information type TargetWithSite = Target & { @@ -111,6 +42,7 @@ export async function getTraefikConfig( .select({ // Resource fields resourceId: resources.resourceId, + resourceName: resources.name, fullDomain: resources.fullDomain, ssl: resources.ssl, http: resources.http, @@ -131,8 +63,12 @@ export async function getTraefikConfig( method: targets.method, port: targets.port, internalPort: targets.internalPort, + hcHealth: targetHealthCheck.hcHealth, path: targets.path, pathMatchType: targets.pathMatchType, + rewritePath: targets.rewritePath, + rewritePathType: targets.rewritePathType, + priority: targets.priority, // Site fields siteId: sites.siteId, @@ -144,33 +80,75 @@ export async function getTraefikConfig( .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .leftJoin( + targetHealthCheck, + eq(targetHealthCheck.targetId, targets.targetId) + ) .where( and( eq(targets.enabled, true), eq(resources.enabled, true), - or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)), + or( + eq(sites.exitNodeId, exitNodeId), + and( + isNull(sites.exitNodeId), + sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes + ) + ), + or( + ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets + isNull(targetHealthCheck.hcHealth) // Include targets with no health check record + ), inArray(sites.type, siteTypes), config.getRawConfig().traefik.allow_raw_resources ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true : eq(resources.http, true) ) - ); + ) + .orderBy(desc(targets.priority), targets.targetId); // stable ordering // Group by resource and include targets with their unique site data const resourcesMap = new Map(); resourcesWithTargetsAndSites.forEach((row) => { const resourceId = row.resourceId; - const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths + const resourceName = sanitize(row.resourceName) || ""; + const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths const pathMatchType = row.pathMatchType || ""; + const rewritePath = row.rewritePath || ""; + const rewritePathType = row.rewritePathType || ""; + const priority = row.priority ?? 100; - // Create a unique key combining resourceId and path+pathMatchType - const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-"); + // Create a unique key combining resourceId, path config, and rewrite config + const pathKey = [ + targetPath, + pathMatchType, + rewritePath, + rewritePathType + ] + .filter(Boolean) + .join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); + const key = sanitize(mapKey); - if (!resourcesMap.has(mapKey)) { - resourcesMap.set(mapKey, { + if (!resourcesMap.has(key)) { + const validation = validatePathRewriteConfig( + row.path, + row.pathMatchType, + row.rewritePath, + row.rewritePathType + ); + + if (!validation.isValid) { + logger.error( + `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}` + ); + return; + } + + resourcesMap.set(key, { resourceId: row.resourceId, + name: resourceName, fullDomain: row.fullDomain, ssl: row.ssl, http: row.http, @@ -186,12 +164,15 @@ export async function getTraefikConfig( targets: [], headers: row.headers, path: row.path, // the targets will all have the same path - pathMatchType: row.pathMatchType // the targets will all have the same pathMatchType + pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType + rewritePath: row.rewritePath, + rewritePathType: row.rewritePathType, + priority: priority // may be null, we fallback later }); } // Add target with its associated site data - resourcesMap.get(mapKey).targets.push({ + resourcesMap.get(key).targets.push({ resourceId: row.resourceId, targetId: row.targetId, ip: row.ip, @@ -230,8 +211,8 @@ export async function getTraefikConfig( for (const [key, resource] of resourcesMap.entries()) { const targets = resource.targets; - const routerName = `${key}-router`; - const serviceName = `${key}-service`; + const routerName = `${key}-${resource.name}-router`; + const serviceName = `${key}-${resource.name}-service`; const fullDomain = `${resource.fullDomain}`; const transportName = `${key}-transport`; const headersMiddlewareName = `${key}-headers-middleware`; @@ -241,19 +222,14 @@ export async function getTraefikConfig( } if (resource.http) { - if (!resource.domainId) { + if (!resource.domainId || !resource.fullDomain) { continue; } - if (!resource.fullDomain) { - continue; - } - - // add routers and services empty objects if they don't exist + // Initialize routers and services if they don't exist if (!config_output.http.routers) { config_output.http.routers = {}; } - if (!config_output.http.services) { config_output.http.services = {}; } @@ -282,21 +258,18 @@ export async function getTraefikConfig( preferWildcardCert = configDomain.prefer_wildcard_cert; } - let tls = {}; - if (build == "oss") { - tls = { - certResolver: certResolver, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; - } + const tls = { + certResolver: certResolver, + ...(preferWildcardCert + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; @@ -306,9 +279,59 @@ export async function getTraefikConfig( ...additionalMiddlewares ]; + // Handle path rewriting middleware + if ( + resource.rewritePath !== null && + resource.path !== null && + resource.pathMatchType && + resource.rewritePathType + ) { + // Create a unique middleware name + const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; + + try { + const rewriteResult = createPathRewriteMiddleware( + rewriteMiddlewareName, + resource.path, + resource.pathMatchType, + resource.rewritePath, + resource.rewritePathType + ); + + // Initialize middlewares object if it doesn't exist + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + + // the middleware to the config + Object.assign( + config_output.http.middlewares, + rewriteResult.middlewares + ); + + // middlewares to the router middleware chain + if (rewriteResult.chain) { + // For chained middlewares (like stripPrefix + addPrefix) + routerMiddlewares.push(...rewriteResult.chain); + } else { + // Single middleware + routerMiddlewares.push(rewriteMiddlewareName); + } + + logger.debug( + `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` + ); + } catch (error) { + logger.error( + `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` + ); + } + } + + // Handle custom headers middleware if (resource.headers || resource.setHostHeader) { - // if there are headers, parse them into an object const headersObj: { [key: string]: string } = {}; + if (resource.headers) { let headersArr: { name: string; value: string }[] = []; try { @@ -331,9 +354,7 @@ export async function getTraefikConfig( headersObj["Host"] = resource.setHostHeader; } - // check if the object is not empty if (Object.keys(headersObj).length > 0) { - // Add the headers middleware if (!config_output.http.middlewares) { config_output.http.middlewares = {}; } @@ -347,17 +368,44 @@ export async function getTraefikConfig( } } + // Build routing rules let rule = `Host(\`${fullDomain}\`)`; - let priority = 100; + + // priority logic + let priority: number; + if (resource.priority && resource.priority != 100) { + priority = resource.priority; + } else { + priority = 100; + if (resource.path && resource.pathMatchType) { + priority += 10; + if (resource.pathMatchType === "exact") { + priority += 5; + } else if (resource.pathMatchType === "prefix") { + priority += 3; + } else if (resource.pathMatchType === "regex") { + priority += 2; + } + if (resource.path === "/") { + priority = 1; // lowest for catch-all + } + } + } + if (resource.path && resource.pathMatchType) { - priority += 1; + // priority += 1; // add path to rule based on match type + let path = resource.path; + // if the path doesn't start with a /, add it + if (!path.startsWith("/")) { + path = `/${path}`; + } if (resource.pathMatchType === "exact") { - rule += ` && Path(\`${resource.path}\`)`; + rule += ` && Path(\`${path}\`)`; } else if (resource.pathMatchType === "prefix") { - rule += ` && PathPrefix(\`${resource.path}\`)`; + rule += ` && PathPrefix(\`${path}\`)`; } else if (resource.pathMatchType === "regex") { - rule += ` && PathRegexp(\`${resource.path}\`)`; + rule += ` && PathRegexp(\`${resource.path}\`)`; // this is the raw path because it's a regex } } @@ -489,7 +537,7 @@ export async function getTraefikConfig( } } else { // Non-HTTP (TCP/UDP) configuration - if (!resource.enableProxy) { + if (!resource.enableProxy || !resource.proxyPort) { continue; } @@ -582,13 +630,3 @@ export async function getTraefikConfig( } return config_output; } - -function sanitizePath(path: string | null | undefined): string | undefined { - if (!path) return undefined; - // clean any non alphanumeric characters from the path and replace with dashes - // the path cant be too long either, so limit to 50 characters - if (path.length > 50) { - path = path.substring(0, 50); - } - return path.replace(/[^a-zA-Z0-9]/g, ""); -} diff --git a/server/lib/traefik/index.ts b/server/lib/traefik/index.ts new file mode 100644 index 00000000..5630028c --- /dev/null +++ b/server/lib/traefik/index.ts @@ -0,0 +1 @@ +export * from "./getTraefikConfig"; \ No newline at end of file diff --git a/server/lib/traefik/middleware.ts b/server/lib/traefik/middleware.ts new file mode 100644 index 00000000..e4055976 --- /dev/null +++ b/server/lib/traefik/middleware.ts @@ -0,0 +1,140 @@ +import logger from "@server/logger"; + +export default function createPathRewriteMiddleware( + middlewareName: string, + path: string, + pathMatchType: string, + rewritePath: string, + rewritePathType: string +): { middlewares: { [key: string]: any }; chain?: string[] } { + const middlewares: { [key: string]: any } = {}; + + if (pathMatchType !== "regex" && !path.startsWith("/")) { + path = `/${path}`; + } + + if ( + rewritePathType !== "regex" && + rewritePath !== "" && + !rewritePath.startsWith("/") + ) { + rewritePath = `/${rewritePath}`; + } + + switch (rewritePathType) { + case "exact": + // Replace the path with the exact rewrite path + const exactPattern = `^${escapeRegex(path)}$`; + middlewares[middlewareName] = { + replacePathRegex: { + regex: exactPattern, + replacement: rewritePath + } + }; + break; + + case "prefix": + // Replace matched prefix with new prefix, preserve the rest + switch (pathMatchType) { + case "prefix": + middlewares[middlewareName] = { + replacePathRegex: { + regex: `^${escapeRegex(path)}(.*)`, + replacement: `${rewritePath}$1` + } + }; + break; + case "exact": + middlewares[middlewareName] = { + replacePathRegex: { + regex: `^${escapeRegex(path)}$`, + replacement: rewritePath + } + }; + break; + case "regex": + // For regex path matching with prefix rewrite, we assume the regex has capture groups + middlewares[middlewareName] = { + replacePathRegex: { + regex: path, + replacement: rewritePath + } + }; + break; + } + break; + + case "regex": + // Use advanced regex replacement - works with any match type + let regexPattern: string; + if (pathMatchType === "regex") { + regexPattern = path; + } else if (pathMatchType === "prefix") { + regexPattern = `^${escapeRegex(path)}(.*)`; + } else { + // exact + regexPattern = `^${escapeRegex(path)}$`; + } + + middlewares[middlewareName] = { + replacePathRegex: { + regex: regexPattern, + replacement: rewritePath + } + }; + break; + + case "stripPrefix": + // Strip the matched prefix and optionally add new path + if (pathMatchType === "prefix") { + middlewares[middlewareName] = { + stripPrefix: { + prefixes: [path] + } + }; + + // If rewritePath is provided and not empty, add it as a prefix after stripping + if (rewritePath && rewritePath !== "" && rewritePath !== "/") { + const addPrefixMiddlewareName = `addprefix-${middlewareName.replace("rewrite-", "")}`; + middlewares[addPrefixMiddlewareName] = { + addPrefix: { + prefix: rewritePath + } + }; + return { + middlewares, + chain: [middlewareName, addPrefixMiddlewareName] + }; + } + } else { + // For exact and regex matches, use replacePathRegex to strip + let regexPattern: string; + if (pathMatchType === "exact") { + regexPattern = `^${escapeRegex(path)}$`; + } else if (pathMatchType === "regex") { + regexPattern = path; + } else { + regexPattern = `^${escapeRegex(path)}`; + } + + const replacement = rewritePath || "/"; + middlewares[middlewareName] = { + replacePathRegex: { + regex: regexPattern, + replacement: replacement + } + }; + } + break; + + default: + logger.error(`Unknown rewritePathType: ${rewritePathType}`); + throw new Error(`Unknown rewritePathType: ${rewritePathType}`); + } + + return { middlewares }; +} + +function escapeRegex(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/server/lib/traefikConfig.test.ts b/server/lib/traefik/traefikConfig.test.ts similarity index 99% rename from server/lib/traefikConfig.test.ts rename to server/lib/traefik/traefikConfig.test.ts index 55d19647..88e5da49 100644 --- a/server/lib/traefikConfig.test.ts +++ b/server/lib/traefik/traefikConfig.test.ts @@ -1,5 +1,5 @@ import { assertEquals } from "@test/assert"; -import { isDomainCoveredByWildcard } from "./traefikConfig"; +import { isDomainCoveredByWildcard } from "./TraefikConfigManager"; function runTests() { console.log('Running wildcard domain coverage tests...'); diff --git a/server/lib/traefik/utils.ts b/server/lib/traefik/utils.ts new file mode 100644 index 00000000..37ebfa0b --- /dev/null +++ b/server/lib/traefik/utils.ts @@ -0,0 +1,81 @@ +import logger from "@server/logger"; + +export function sanitize(input: string | null | undefined): string | undefined { + if (!input) return undefined; + // clean any non alphanumeric characters from the input and replace with dashes + // the input cant be too long either, so limit to 50 characters + if (input.length > 50) { + input = input.substring(0, 50); + } + return input + .replace(/[^a-zA-Z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +export function validatePathRewriteConfig( + path: string | null, + pathMatchType: string | null, + rewritePath: string | null, + rewritePathType: string | null +): { isValid: boolean; error?: string } { + // If no path matching is configured, no rewriting is possible + if (!path || !pathMatchType) { + if (rewritePath || rewritePathType) { + return { + isValid: false, + error: "Path rewriting requires path matching to be configured" + }; + } + return { isValid: true }; + } + + if (rewritePathType !== "stripPrefix") { + if ((rewritePath && !rewritePathType) || (!rewritePath && rewritePathType)) { + return { isValid: false, error: "Both rewritePath and rewritePathType must be specified together" }; + } + } + + + if (!rewritePath || !rewritePathType) { + return { isValid: true }; + } + + const validPathMatchTypes = ["exact", "prefix", "regex"]; + if (!validPathMatchTypes.includes(pathMatchType)) { + return { + isValid: false, + error: `Invalid pathMatchType: ${pathMatchType}. Must be one of: ${validPathMatchTypes.join(", ")}` + }; + } + + const validRewritePathTypes = ["exact", "prefix", "regex", "stripPrefix"]; + if (!validRewritePathTypes.includes(rewritePathType)) { + return { + isValid: false, + error: `Invalid rewritePathType: ${rewritePathType}. Must be one of: ${validRewritePathTypes.join(", ")}` + }; + } + + if (pathMatchType === "regex") { + try { + new RegExp(path); + } catch (e) { + return { + isValid: false, + error: `Invalid regex pattern in path: ${path}` + }; + } + } + + + // 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}`); + } + } + + return { isValid: true }; +} + diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 522e5018..59776105 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -163,6 +163,26 @@ export function validateHeaders(headers: string): boolean { }); } +export function isSecondLevelDomain(domain: string): boolean { + if (!domain || typeof domain !== 'string') { + return false; + } + + const trimmedDomain = domain.trim().toLowerCase(); + + // Split into parts + const parts = trimmedDomain.split('.'); + + // Should have exactly 2 parts for a second-level domain (e.g., "example.com") + if (parts.length !== 2) { + return false; + } + + // Check if the TLD part is valid + const tld = parts[1].toUpperCase(); + return validTlds.includes(tld); +} + const validTlds = [ "AAA", "AARP", diff --git a/server/logger.ts b/server/logger.ts index cd12d735..99b0cfbf 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -4,6 +4,22 @@ import * as winston from "winston"; import path from "path"; import { APP_PATH } from "./lib/consts"; +// helper to get ISO8601 string in the TZ from process.env.TZ +// This replaces the default Z (UTC) with the local offset from process.env.TZ +const isoLocal = () => { + const tz = process.env.TZ || "UTC"; + const d = new Date(); + const s = d.toLocaleString("sv-SE", { timeZone: tz, hour12: false }); + const tzOffsetMin = d.getTimezoneOffset(); + const sign = tzOffsetMin <= 0 ? "+" : "-"; + const pad = (n: number) => String(n).padStart(2, "0"); + const hours = pad(Math.floor(Math.abs(tzOffsetMin) / 60)); + const mins = pad(Math.abs(tzOffsetMin) % 60); + + // Replace Z in ISO string with local offset + return s.replace(" ", "T") + `${sign}${hours}:${mins}`; +}; + const hformat = winston.format.printf( ({ level, label, message, timestamp, stack, ...metadata }) => { let msg = `${timestamp} [${level}]${label ? `[${label}]` : ""}: ${message}`; @@ -28,7 +44,12 @@ if (config.getRawConfig().app.save_logs) { maxSize: "20m", maxFiles: "7d", createSymlink: true, - symlinkName: "pangolin.log" + symlinkName: "pangolin.log", + format: winston.format.combine( + winston.format.timestamp({ format: isoLocal }), + winston.format.splat(), + hformat + ) }) ); transports.push( @@ -41,7 +62,7 @@ if (config.getRawConfig().app.save_logs) { createSymlink: true, symlinkName: ".machinelogs.json", format: winston.format.combine( - winston.format.timestamp(), + winston.format.timestamp({ format: isoLocal }), winston.format.splat(), winston.format.json() ) @@ -55,7 +76,9 @@ const logger = winston.createLogger({ winston.format.errors({ stack: true }), winston.format.colorize(), winston.format.splat(), - winston.format.timestamp(), + + // Use isoLocal so timestamps respect TZ env, not just UTC + winston.format.timestamp({ format: isoLocal }), hformat ), transports diff --git a/server/middlewares/stripDuplicateSessions.ts b/server/middlewares/stripDuplicateSessions.ts new file mode 100644 index 00000000..2558e511 --- /dev/null +++ b/server/middlewares/stripDuplicateSessions.ts @@ -0,0 +1,49 @@ +import { NextFunction, Response } from "express"; +import ErrorResponse from "@server/types/ErrorResponse"; +import { + SESSION_COOKIE_NAME, + validateSessionToken +} from "@server/auth/sessions/app"; + +export const stripDuplicateSesions = async ( + req: any, + res: Response, + next: NextFunction +) => { + const cookieHeader: string | undefined = req.headers.cookie; + if (!cookieHeader) { + return next(); + } + + const cookies = cookieHeader.split(";").map((cookie) => cookie.trim()); + const sessionCookies = cookies.filter((cookie) => + cookie.startsWith(`${SESSION_COOKIE_NAME}=`) + ); + + const validSessions: string[] = []; + if (sessionCookies.length > 1) { + for (const cookie of sessionCookies) { + const cookieValue = cookie.split("=")[1]; + const res = await validateSessionToken(cookieValue); + if (res.session && res.user) { + validSessions.push(cookieValue); + } + } + + if (validSessions.length > 0) { + const newCookieHeader = cookies.filter((cookie) => { + if (cookie.startsWith(`${SESSION_COOKIE_NAME}=`)) { + const cookieValue = cookie.split("=")[1]; + return validSessions.includes(cookieValue); + } + return true; + }); + req.headers.cookie = newCookieHeader.join("; "); + if (req.cookies) { + req.cookies[SESSION_COOKIE_NAME] = validSessions[0]; + } + } + } + + return next(); +}; diff --git a/server/nextServer.ts b/server/nextServer.ts index 4c96d04f..78169f03 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -3,6 +3,7 @@ import express from "express"; import { parse } from "url"; import logger from "@server/logger"; import config from "@server/lib/config"; +import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; const nextPort = config.getRawConfig().server.next_port; @@ -15,6 +16,8 @@ export async function createNextServer() { const nextServer = express(); + nextServer.use(stripDuplicateSesions); + nextServer.all("/{*splat}", (req, res) => { const parsedUrl = parse(req.url!, true); return handle(req, res, parsedUrl); diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index 3a9120e3..64efb696 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { db } from "@server/db"; import { User, users } from "@server/db"; import { eq } from "drizzle-orm"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { hashPassword, verifyPassword diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts index ca7d80cc..9840d564 100644 --- a/server/routers/auth/checkResourceSession.ts +++ b/server/routers/auth/checkResourceSession.ts @@ -3,7 +3,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import logger from "@server/logger"; diff --git a/server/routers/auth/disable2fa.ts b/server/routers/auth/disable2fa.ts index 7fbea2e5..da19c0d7 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { db } from "@server/db"; import { User, users } from "@server/db"; import { eq } from "drizzle-orm"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { verifyPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/totp"; import logger from "@server/logger"; diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index 505d12c2..754478fc 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -13,4 +13,4 @@ export * from "./initialSetupComplete"; export * from "./validateSetupToken"; export * from "./changePassword"; export * from "./checkResourceSession"; -export * from "./securityKey"; +export * from "./securityKey"; \ No newline at end of file diff --git a/server/routers/auth/initialSetupComplete.ts b/server/routers/auth/initialSetupComplete.ts index 8da9acd7..2b616c97 100644 --- a/server/routers/auth/initialSetupComplete.ts +++ b/server/routers/auth/initialSetupComplete.ts @@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db, users } from "@server/db"; import { eq } from "drizzle-orm"; diff --git a/server/routers/auth/requestEmailVerificationCode.ts b/server/routers/auth/requestEmailVerificationCode.ts index eeabedf2..7358e6ed 100644 --- a/server/routers/auth/requestEmailVerificationCode.ts +++ b/server/routers/auth/requestEmailVerificationCode.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { User } from "@server/db"; import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode"; import config from "@server/lib/config"; diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index 62951ab1..52dce2e3 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -3,7 +3,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db } from "@server/db"; import { passwordResetTokens, users } from "@server/db"; import { eq } from "drizzle-orm"; diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index 753867b6..7c122a44 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import { encodeHex } from "oslo/encoding"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db } from "@server/db"; import { User, users } from "@server/db"; import { eq, and } from "drizzle-orm"; @@ -110,10 +110,12 @@ export async function requestTotpSecret( ); } + const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config + const hex = crypto.getRandomValues(new Uint8Array(20)); const secret = encodeHex(hex); const uri = createTOTPKeyURI( - "Pangolin", + appName, user.email!, hex ); diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index 8ae62eb0..05293727 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -4,7 +4,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db } from "@server/db"; import { passwordResetTokens, users } from "@server/db"; import { eq } from "drizzle-orm"; diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index 6b014986..1e75764b 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { db } from "@server/db"; import { User, securityKeys, users, webauthnChallenge } from "@server/db"; import { eq, and, lt } from "drizzle-orm"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import logger from "@server/logger"; import { generateRegistrationOptions, @@ -16,18 +16,12 @@ import { } from "@simplewebauthn/server"; import type { GenerateRegistrationOptionsOpts, - VerifyRegistrationResponseOpts, GenerateAuthenticationOptionsOpts, - VerifyAuthenticationResponseOpts, - VerifiedRegistrationResponse, - VerifiedAuthenticationResponse + AuthenticatorTransportFuture } from "@simplewebauthn/server"; -import type { - AuthenticatorTransport, - AuthenticatorTransportFuture, - PublicKeyCredentialDescriptorJSON, - PublicKeyCredentialDescriptorFuture -} from "@simplewebauthn/types"; +import { + isoBase64URL +} from '@simplewebauthn/server/helpers'; import config from "@server/lib/config"; import { UserType } from "@server/types/UserTypes"; import { verifyPassword } from "@server/auth/password"; @@ -204,15 +198,14 @@ export async function startRegistration( .where(eq(securityKeys.userId, user.userId)); const excludeCredentials = existingSecurityKeys.map(key => ({ - id: new Uint8Array(Buffer.from(key.credentialId, 'base64')), - type: 'public-key' as const, + id: key.credentialId, transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined })); const options: GenerateRegistrationOptionsOpts = { rpName, rpID, - userID: user.userId, + userID: isoBase64URL.toBuffer(user.userId), userName: user.email || user.username, attestationType: 'none', excludeCredentials, @@ -308,11 +301,11 @@ export async function verifyRegistration( // Store the security key in the database await db.insert(securityKeys).values({ - credentialId: Buffer.from(registrationInfo.credentialID).toString('base64'), + credentialId: registrationInfo.credential.id, userId: user.userId, - publicKey: Buffer.from(registrationInfo.credentialPublicKey).toString('base64'), - signCount: registrationInfo.counter || 0, - transports: credential.response.transports ? JSON.stringify(credential.response.transports) : null, + publicKey: isoBase64URL.fromBuffer(registrationInfo.credential.publicKey), + signCount: registrationInfo.credential.counter || 0, + transports: registrationInfo.credential.transports ? JSON.stringify(registrationInfo.credential.transports) : null, name: challengeData.securityKeyName, lastUsed: new Date().toISOString(), dateCreated: new Date().toISOString() @@ -496,7 +489,7 @@ export async function startAuthentication( const { email } = parsedBody.data; try { - let allowCredentials: PublicKeyCredentialDescriptorFuture[] = []; + let allowCredentials; let userId; // If email is provided, get security keys for that specific user @@ -533,13 +526,9 @@ export async function startAuthentication( } allowCredentials = userSecurityKeys.map(key => ({ - id: new Uint8Array(Buffer.from(key.credentialId, 'base64')), - type: 'public-key' as const, + id: key.credentialId, transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined })); - } else { - // If no email provided, allow any security key (for resident key authentication) - allowCredentials = []; } const options: GenerateAuthenticationOptionsOpts = { @@ -616,7 +605,7 @@ export async function verifyAuthentication( } // Find the security key in database - const credentialId = Buffer.from(credential.id, 'base64').toString('base64'); + const credentialId = credential.id; const [securityKey] = await db .select() .from(securityKeys) @@ -653,9 +642,9 @@ export async function verifyAuthentication( expectedChallenge: challengeData.challenge, expectedOrigin: origin, expectedRPID: rpID, - authenticator: { - credentialID: Buffer.from(securityKey.credentialId, 'base64'), - credentialPublicKey: Buffer.from(securityKey.publicKey, 'base64'), + credential: { + id: securityKey.credentialId, + publicKey: isoBase64URL.toBuffer(securityKey.publicKey), counter: securityKey.signCount, transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransportFuture[] : undefined }, @@ -714,4 +703,4 @@ export async function verifyAuthentication( ) ); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts index ebb95359..716feca4 100644 --- a/server/routers/auth/setServerAdmin.ts +++ b/server/routers/auth/setServerAdmin.ts @@ -7,7 +7,7 @@ import { generateId } from "@server/auth/sessions/app"; import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { passwordSchema } from "@server/auth/passwordSchema"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db, users, setupTokens } from "@server/db"; import { eq, and } from "drizzle-orm"; import { UserType } from "@server/types/UserTypes"; diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 09c8db07..fe978d0d 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -21,14 +21,12 @@ import { hashPassword } from "@server/auth/password"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; -import { build } from "@server/build"; export const signupBodySchema = z.object({ email: z.string().toLowerCase().email(), password: passwordSchema, inviteToken: z.string().optional(), - inviteId: z.string().optional(), - termsAcceptedTimestamp: z.string().nullable().optional() + inviteId: z.string().optional() }); export type SignUpBody = z.infer; @@ -53,7 +51,7 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } = + const { email, password, inviteToken, inviteId } = parsedBody.data; const passwordHash = await hashPassword(password); @@ -161,15 +159,6 @@ export async function signup( } } - if (build === "saas" && !termsAcceptedTimestamp) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "You must accept the terms of service and privacy policy" - ) - ); - } - await db.insert(users).values({ userId: userId, type: UserType.Internal, @@ -177,7 +166,7 @@ export async function signup( email: email, passwordHash, dateCreated: moment().toISOString(), - termsAcceptedTimestamp: termsAcceptedTimestamp || null, + termsAcceptedTimestamp: null, termsVersion: "1" }); diff --git a/server/routers/auth/types.ts b/server/routers/auth/types.ts new file mode 100644 index 00000000..bb5a1b4e --- /dev/null +++ b/server/routers/auth/types.ts @@ -0,0 +1,8 @@ +export type TransferSessionResponse = { + valid: boolean; + cookie?: string; +}; + +export type GetSessionTransferTokenRenponse = { + token: string; +}; \ No newline at end of file diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index f707de22..c624e747 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -3,7 +3,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db } from "@server/db"; import { User, emailVerificationCodes, users } from "@server/db"; import { eq } from "drizzle-orm"; diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index 6b45a93e..c44c0c53 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -3,7 +3,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import { db } from "@server/db"; import { twoFactorBackupCodes, User, users } from "@server/db"; import { eq, and } from "drizzle-orm"; diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index d6f2c7c7..b4b2deea 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -15,7 +15,7 @@ import { 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"; +import { response } from "@server/lib/response"; const exchangeSessionBodySchema = z.object({ requestToken: z.string(), diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 120aa528..87eeac97 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -7,20 +7,20 @@ import { import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { getResourceByDomain, - getUserSessionWithUser, getUserOrgRoles, + getResourceRules, getRoleResourceAccess, getUserResourceAccess, - getResourceRules + getUserSessionWithUser } from "@server/db/queries/verifySessionQueries"; import { + LoginPage, Resource, ResourceAccessToken, + ResourceHeaderAuth, ResourcePassword, ResourcePincode, - ResourceRule, - sessions, - users + ResourceRule } from "@server/db"; import config from "@server/lib/config"; import { isIpInCidr } from "@server/lib/ip"; @@ -32,7 +32,8 @@ import createHttpError from "http-errors"; import NodeCache from "node-cache"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { getCountryCodeForIp } from "@server/lib"; +import { getCountryCodeForIp } from "@server/lib/geoip"; +import { verifyPassword } from "@server/auth/password"; // We'll see if this speeds anything up const cache = new NodeCache({ @@ -97,6 +98,9 @@ export async function verifyResourceSession( query } = parsedBody.data; + // Extract HTTP Basic Auth credentials if present + const clientHeaderAuth = extractBasicAuth(headers); + const clientIp = requestIp ? (() => { logger.debug("Request IP:", { requestIp }); @@ -133,6 +137,7 @@ export async function verifyResourceSession( resource: Resource | null; pincode: ResourcePincode | null; password: ResourcePassword | null; + headerAuth: ResourceHeaderAuth | null; } | undefined = cache.get(resourceCacheKey); @@ -148,7 +153,7 @@ export async function verifyResourceSession( cache.set(resourceCacheKey, resourceData); } - const { resource, pincode, password } = resourceData; + const { resource, pincode, password, headerAuth } = resourceData; if (!resource) { logger.debug(`Resource not found ${cleanHost}`); @@ -186,27 +191,20 @@ export async function verifyResourceSession( // otherwise its undefined and we pass } + // IMPORTANT: ADD NEW AUTH CHECKS HERE OR WHEN TURNING OFF ALL OTHER AUTH METHODS IT WILL JUST PASS if ( - !resource.sso && + !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuth ) { logger.debug("Resource allowed because no auth"); return allowed(res); } - let endpoint: string; - if (config.isManagedMode()) { - endpoint = - config.getRawConfig().managed?.redirect_endpoint || - config.getRawConfig().managed?.endpoint || - ""; - } else { - endpoint = config.getRawConfig().app.dashboard_url!; - } - const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent( - resource.resourceId + const redirectPath = `/auth/resource/${encodeURIComponent( + resource.resourceGuid )}?redirect=${encodeURIComponent(originalRequestURL)}`; // check for access token in headers @@ -293,6 +291,46 @@ export async function verifyResourceSession( } } + // check for HTTP Basic Auth header + const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`; + if (headerAuth && clientHeaderAuth) { + if (cache.get(clientHeaderAuthKey)) { + logger.debug( + "Resource allowed because header auth is valid (cached)" + ); + return allowed(res); + } else if ( + await verifyPassword( + clientHeaderAuth, + headerAuth.headerAuthHash + ) + ) { + cache.set(clientHeaderAuthKey, clientHeaderAuth); + logger.debug("Resource allowed because header auth is valid"); + return allowed(res); + } + + if ( + // we dont want to redirect if this is the only auth method and we did not pass here + !sso && + !pincode && + !password && + !resource.emailWhitelistEnabled + ) { + return notAllowed(res); + } + } else if (headerAuth) { + // if there are no other auth methods we need to return unauthorized if nothing is provided + if ( + !sso && + !pincode && + !password && + !resource.emailWhitelistEnabled + ) { + return notAllowed(res); + } + } + if (!sessions) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( @@ -408,7 +446,10 @@ export async function verifyResourceSession( }. IP: ${clientIp}.` ); } - return notAllowed(res, redirectUrl); + + logger.debug(`Redirecting to login at ${redirectPath}`); + + return notAllowed(res, redirectPath, resource.orgId); } catch (e) { console.error(e); return next( @@ -463,7 +504,34 @@ function extractResourceSessionToken( return latest.token; } -function notAllowed(res: Response, redirectUrl?: string) { +async function notAllowed( + res: Response, + redirectPath?: string, + orgId?: string +) { + // let loginPage: LoginPage | null = null; + /* if (orgId) { + const { tier } = await getOrgTierData(orgId); // returns null in oss + if (tier === TierId.STANDARD) { + loginPage = await getOrgLoginPage(orgId); + } + }*/ + + let redirectUrl: string | undefined = undefined; + if (redirectPath) { + let endpoint: string; + + /* if (loginPage && loginPage.domainId && loginPage.fullDomain) { + const secure = config + .getRawConfig() + .app.dashboard_url?.startsWith("https"); + const method = secure ? "https" : "http"; + endpoint = `${method}://${loginPage.fullDomain}`; + } */ + endpoint = config.getRawConfig().app.dashboard_url!; + redirectUrl = `${endpoint}${redirectPath}`; + } + const data = { data: { valid: false, redirectUrl }, success: true, @@ -489,39 +557,6 @@ function allowed(res: Response, userData?: BasicUserData) { return response(res, data); } -async function createAccessTokenSession( - res: Response, - resource: Resource, - tokenItem: ResourceAccessToken -) { - const token = generateSessionToken(); - const sess = await createResourceSession({ - resourceId: resource.resourceId, - token, - accessTokenId: tokenItem.accessTokenId, - sessionLength: tokenItem.sessionLength, - expiresAt: tokenItem.expiresAt, - doNotExtend: tokenItem.expiresAt ? true : false - }); - const cookieName = `${config.getRawConfig().server.session_cookie_name}`; - const cookie = serializeResourceSessionCookie( - cookieName, - resource.fullDomain!, - token, - !resource.ssl, - new Date(sess.expiresAt) - ); - res.appendHeader("Set-Cookie", cookie); - logger.debug("Access token is valid, creating new session"); - return response(res, { - data: { valid: true }, - success: true, - error: false, - message: "Access allowed", - status: HttpCode.OK - }); -} - async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource @@ -758,7 +793,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise { let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); if (!cachedCountryCode) { - cachedCountryCode = await getCountryCodeForIp(ip); + cachedCountryCode = await getCountryCodeForIp(ip); // do it locally // Cache for longer since IP geolocation doesn't change frequently cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes } @@ -767,3 +802,28 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise { return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase(); } + +function extractBasicAuth( + headers: Record | undefined +): string | undefined { + if (!headers || (!headers.authorization && !headers.Authorization)) { + return; + } + + const authHeader = headers.authorization || headers.Authorization; + + // Check if it's Basic Auth + if (!authHeader.startsWith("Basic ")) { + logger.debug("Authorization header is not Basic Auth"); + return; + } + + try { + // Extract the base64 encoded credentials + return authHeader.slice("Basic ".length); + } catch (error) { + logger.debug("Basic Auth: Failed to decode credentials", { + error: error instanceof Error ? error.message : "Unknown error" + }); + } +} diff --git a/server/routers/certificates/createCertificate.ts b/server/routers/certificates/createCertificate.ts new file mode 100644 index 00000000..e160e644 --- /dev/null +++ b/server/routers/certificates/createCertificate.ts @@ -0,0 +1,5 @@ +import { db, Transaction } from "@server/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 new file mode 100644 index 00000000..80136de8 --- /dev/null +++ b/server/routers/certificates/types.ts @@ -0,0 +1,13 @@ +export type GetCertificateResponse = { + certId: number; + domain: string; + domainId: string; + wildcard: boolean; + status: string; // pending, requested, valid, expired, failed + expiresAt: string | null; + lastRenewalAttempt: Date | null; + createdAt: string; + updatedAt: string; + errorMessage?: string | null; + renewalCount: number; +} \ No newline at end of file diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index e34a23e9..38a95945 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -1,4 +1,4 @@ -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; export async function addTargets( newtId: string, diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 81ee4278..80050f6c 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, exitNodes, sites } from "@server/db"; +import { Client, db, exitNodes, sites } from "@server/db"; import { clients, clientSites } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -17,7 +17,7 @@ import { addPeer as olmAddPeer, deletePeer as olmDeletePeer } from "../olm/peers"; -import { sendToExitNode } from "../../lib/exitNodeComms"; +import { sendToExitNode } from "@server/lib/exitNodes"; const updateClientParamsSchema = z .object({ @@ -29,7 +29,7 @@ const updateClientSchema = z .object({ name: z.string().min(1).max(255).optional(), siteIds: z - .array(z.string().transform(Number).pipe(z.number())) + .array(z.number().int().positive()) .optional() }) .strict(); @@ -105,146 +105,28 @@ export async function updateClient( ); } - if (siteIds) { - let sitesAdded = []; - let sitesRemoved = []; + let sitesAdded = []; + let sitesRemoved = []; - // Fetch existing site associations - const existingSites = await db - .select({ siteId: clientSites.siteId }) - .from(clientSites) - .where(eq(clientSites.clientId, clientId)); + // Fetch existing site associations + const existingSites = await db + .select({ siteId: clientSites.siteId }) + .from(clientSites) + .where(eq(clientSites.clientId, clientId)); - const existingSiteIds = existingSites.map((site) => site.siteId); + const existingSiteIds = existingSites.map((site) => site.siteId); - // Determine which sites were added and removed - sitesAdded = siteIds.filter( - (siteId) => !existingSiteIds.includes(siteId) - ); - sitesRemoved = existingSiteIds.filter( - (siteId) => !siteIds.includes(siteId) - ); - - logger.info( - `Adding ${sitesAdded.length} new sites to client ${client.clientId}` - ); - for (const siteId of sitesAdded) { - if (!client.subnet || !client.pubKey) { - logger.debug( - "Client subnet, pubKey or endpoint is not set" - ); - continue; - } - - // TODO: WE NEED TO HANDLE THIS BETTER. RIGHT NOW WE ARE JUST GUESSING BASED ON THE OTHER SITES - // BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS - const isRelayed = true; - - // get the clientsite - const [clientSite] = await db - .select() - .from(clientSites) - .where( - and( - eq(clientSites.clientId, client.clientId), - eq(clientSites.siteId, siteId) - ) - ) - .limit(1); - - if (!clientSite || !clientSite.endpoint) { - logger.debug("Client site is missing or has no endpoint"); - continue; - } - - const site = await newtAddPeer(siteId, { - publicKey: client.pubKey, - allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: isRelayed ? "" : clientSite.endpoint - }); - - if (!site) { - logger.debug("Failed to add peer to newt - missing site"); - continue; - } - - if (!site.endpoint || !site.publicKey) { - logger.debug("Site endpoint or publicKey is not set"); - continue; - } - - let endpoint; - - if (isRelayed) { - if (!site.exitNodeId) { - logger.warn( - `Site ${site.siteId} has no exit node, skipping` - ); - return null; - } - - // get the exit node for the site - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, site.exitNodeId)) - .limit(1); - - if (!exitNode) { - logger.warn( - `Exit node not found for site ${site.siteId}` - ); - return null; - } - - endpoint = `${exitNode.endpoint}:21820`; - } else { - if (!endpoint) { - logger.warn( - `Site ${site.siteId} has no endpoint, skipping` - ); - return null; - } - endpoint = site.endpoint; - } - - await olmAddPeer(client.clientId, { - siteId: site.siteId, - endpoint: endpoint, - publicKey: site.publicKey, - serverIP: site.address, - serverPort: site.listenPort, - remoteSubnets: site.remoteSubnets - }); - } - - logger.info( - `Removing ${sitesRemoved.length} sites from client ${client.clientId}` - ); - for (const siteId of sitesRemoved) { - if (!client.pubKey) { - logger.debug("Client pubKey is not set"); - continue; - } - const site = await newtDeletePeer(siteId, client.pubKey); - if (!site) { - logger.debug( - "Failed to delete peer from newt - missing site" - ); - continue; - } - if (!site.endpoint || !site.publicKey) { - logger.debug("Site endpoint or publicKey is not set"); - continue; - } - await olmDeletePeer( - client.clientId, - site.siteId, - site.publicKey - ); - } - } + const siteIdsToProcess = siteIds || []; + // Determine which sites were added and removed + sitesAdded = siteIdsToProcess.filter( + (siteId) => !existingSiteIds.includes(siteId) + ); + sitesRemoved = existingSiteIds.filter( + (siteId) => !siteIdsToProcess.includes(siteId) + ); + let updatedClient: Client | undefined = undefined; + let sitesData: any; // TODO: define type somehow from the query below await db.transaction(async (trx) => { // Update client name if provided if (name) { @@ -255,133 +137,241 @@ export async function updateClient( } // Update site associations if provided - if (siteIds) { - // Delete existing site associations + // Remove sites that are no longer associated + for (const siteId of sitesRemoved) { await trx .delete(clientSites) - .where(eq(clientSites.clientId, clientId)); - - // Create new site associations - if (siteIds.length > 0) { - await trx.insert(clientSites).values( - siteIds.map((siteId) => ({ - clientId, - siteId - })) + .where( + and( + eq(clientSites.clientId, clientId), + eq(clientSites.siteId, siteId) + ) ); - } } - // get all sites for this client and join with exit nodes with site.exitNodeId - const sitesData = await db - .select() - .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) - .where(eq(clientSites.clientId, client.clientId)); - - let exitNodeDestinations: { - reachableAt: string; - exitNodeId: number; - type: string; - sourceIp: string; - sourcePort: number; - destinations: PeerDestination[]; - }[] = []; - - for (const site of sitesData) { - if (!site.sites.subnet) { - logger.warn( - `Site ${site.sites.siteId} has no subnet, skipping` - ); - continue; - } - - if (!site.clientSites.endpoint) { - logger.warn( - `Site ${site.sites.siteId} has no endpoint, skipping` - ); - continue; - } - - // find the destinations in the array - let destinations = exitNodeDestinations.find( - (d) => d.reachableAt === site.exitNodes?.reachableAt - ); - - if (!destinations) { - destinations = { - reachableAt: site.exitNodes?.reachableAt || "", - exitNodeId: site.exitNodes?.exitNodeId || 0, - type: site.exitNodes?.type || "", - sourceIp: site.clientSites.endpoint.split(":")[0] || "", - sourcePort: - parseInt(site.clientSites.endpoint.split(":")[1]) || - 0, - destinations: [ - { - destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 - } - ] - }; - } else { - // add to the existing destinations - destinations.destinations.push({ - destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 - }); - } - - // update it in the array - exitNodeDestinations = exitNodeDestinations.filter( - (d) => d.reachableAt !== site.exitNodes?.reachableAt - ); - exitNodeDestinations.push(destinations); - } - - for (const destination of exitNodeDestinations) { - logger.info( - `Updating destinations for exit node at ${destination.reachableAt}` - ); - const payload = { - sourceIp: destination.sourceIp, - sourcePort: destination.sourcePort, - destinations: destination.destinations - }; - logger.info( - `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` - ); - - // Create an ExitNode-like object for sendToExitNode - const exitNodeForComm = { - exitNodeId: destination.exitNodeId, - type: destination.type, - reachableAt: destination.reachableAt - } as any; // Using 'as any' since we know sendToExitNode will handle this correctly - - await sendToExitNode(exitNodeForComm, { - remoteType: "remoteExitNode/update-destinations", - localPath: "/update-destinations", - method: "POST", - data: payload + // Add new site associations + for (const siteId of sitesAdded) { + await trx.insert(clientSites).values({ + clientId, + siteId }); } // Fetch the updated client - const [updatedClient] = await trx + [updatedClient] = await trx .select() .from(clients) .where(eq(clients.clientId, clientId)) .limit(1); - return response(res, { - data: updatedClient, - success: true, - error: false, - message: "Client updated successfully", - status: HttpCode.OK + // get all sites for this client and join with exit nodes with site.exitNodeId + sitesData = await trx + .select() + .from(sites) + .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) + .where(eq(clientSites.clientId, client.clientId)); + }); + + logger.info( + `Adding ${sitesAdded.length} new sites to client ${client.clientId}` + ); + for (const siteId of sitesAdded) { + if (!client.subnet || !client.pubKey) { + logger.debug("Client subnet, pubKey or endpoint is not set"); + continue; + } + + // TODO: WE NEED TO HANDLE THIS BETTER. WE ARE DEFAULTING TO RELAYING FOR NEW SITES + // BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS + // AND TRIGGER A HOLEPUNCH OR SOMETHING TO GET THE ENDPOINT AND HP TO THE NEW SITES + const isRelayed = true; + + const site = await newtAddPeer(siteId, { + publicKey: client.pubKey, + allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client + // endpoint: isRelayed ? "" : clientSite.endpoint + endpoint: isRelayed ? "" : "" // we are not HPing yet so no endpoint }); + + if (!site) { + logger.debug("Failed to add peer to newt - missing site"); + continue; + } + + if (!site.endpoint || !site.publicKey) { + logger.debug("Site endpoint or publicKey is not set"); + continue; + } + + let endpoint; + + if (isRelayed) { + if (!site.exitNodeId) { + logger.warn( + `Site ${site.siteId} has no exit node, skipping` + ); + return null; + } + + // get the exit node for the site + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + + if (!exitNode) { + logger.warn(`Exit node not found for site ${site.siteId}`); + return null; + } + + endpoint = `${exitNode.endpoint}:21820`; + } else { + if (!site.endpoint) { + logger.warn( + `Site ${site.siteId} has no endpoint, skipping` + ); + return null; + } + endpoint = site.endpoint; + } + + await olmAddPeer(client.clientId, { + siteId: site.siteId, + endpoint: endpoint, + publicKey: site.publicKey, + serverIP: site.address, + serverPort: site.listenPort, + remoteSubnets: site.remoteSubnets + }); + } + + logger.info( + `Removing ${sitesRemoved.length} sites from client ${client.clientId}` + ); + for (const siteId of sitesRemoved) { + if (!client.pubKey) { + logger.debug("Client pubKey is not set"); + continue; + } + const site = await newtDeletePeer(siteId, client.pubKey); + if (!site) { + logger.debug("Failed to delete peer from newt - missing site"); + continue; + } + if (!site.endpoint || !site.publicKey) { + logger.debug("Site endpoint or publicKey is not set"); + continue; + } + await olmDeletePeer(client.clientId, site.siteId, site.publicKey); + } + + if (!updatedClient || !sitesData) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Failed to update client` + ) + ); + } + + let exitNodeDestinations: { + reachableAt: string; + exitNodeId: number; + type: string; + name: string; + sourceIp: string; + sourcePort: number; + destinations: PeerDestination[]; + }[] = []; + + for (const site of sitesData) { + if (!site.sites.subnet) { + logger.warn( + `Site ${site.sites.siteId} has no subnet, skipping` + ); + continue; + } + + if (!site.clientSites.endpoint) { + logger.warn( + `Site ${site.sites.siteId} has no endpoint, skipping` + ); + continue; + } + + // find the destinations in the array + let destinations = exitNodeDestinations.find( + (d) => d.reachableAt === site.exitNodes?.reachableAt + ); + + if (!destinations) { + destinations = { + reachableAt: site.exitNodes?.reachableAt || "", + exitNodeId: site.exitNodes?.exitNodeId || 0, + type: site.exitNodes?.type || "", + name: site.exitNodes?.name || "", + sourceIp: site.clientSites.endpoint.split(":")[0] || "", + sourcePort: + parseInt(site.clientSites.endpoint.split(":")[1]) || 0, + destinations: [ + { + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + } + ] + }; + } else { + // add to the existing destinations + destinations.destinations.push({ + destinationIP: site.sites.subnet.split("/")[0], + destinationPort: site.sites.listenPort || 0 + }); + } + + // update it in the array + exitNodeDestinations = exitNodeDestinations.filter( + (d) => d.reachableAt !== site.exitNodes?.reachableAt + ); + exitNodeDestinations.push(destinations); + } + + for (const destination of exitNodeDestinations) { + logger.info( + `Updating destinations for exit node at ${destination.reachableAt}` + ); + const payload = { + sourceIp: destination.sourceIp, + sourcePort: destination.sourcePort, + destinations: destination.destinations + }; + logger.info( + `Payload for update-destinations: ${JSON.stringify(payload, null, 2)}` + ); + + // Create an ExitNode-like object for sendToExitNode + const exitNodeForComm = { + exitNodeId: destination.exitNodeId, + type: destination.type, + reachableAt: destination.reachableAt, + name: destination.name + } as any; // Using 'as any' since we know sendToExitNode will handle this correctly + + await sendToExitNode(exitNodeForComm, { + remoteType: "remoteExitNode/update-destinations", + localPath: "/update-destinations", + method: "POST", + data: payload + }); + } + + return response(res, { + data: updatedClient, + success: true, + error: false, + message: "Client updated successfully", + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 08718d44..e39c09d3 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -9,8 +9,7 @@ import { fromError } from "zod-validation-error"; import { subdomainSchema } from "@server/lib/schemas"; import { generateId } from "@server/auth/sessions/app"; import { eq, and } from "drizzle-orm"; -import { isValidDomain } from "@server/lib/validators"; -import { build } from "@server/build"; +import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; import config from "@server/lib/config"; const paramsSchema = z @@ -71,25 +70,15 @@ export async function createOrgDomain( const { orgId } = parsedParams.data; const { type, baseDomain } = parsedBody.data; - if (build == "oss") { - if (type !== "wildcard") { - return next( - createHttpError( - HttpCode.NOT_IMPLEMENTED, - "Creating NS or CNAME records is not supported" - ) - ); - } - } else if (build == "enterprise" || build == "saas") { - if (type !== "ns" && type !== "cname") { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid domain type. Only NS, CNAME are allowed." - ) - ); - } + if (type !== "wildcard") { + return next( + createHttpError( + HttpCode.NOT_IMPLEMENTED, + "Creating NS or CNAME records is not supported" + ) + ); } + // allow wildacard, cname, and ns in enterprise // Validate organization exists if (!isValidDomain(baseDomain)) { @@ -98,6 +87,16 @@ export async function createOrgDomain( ); } + /* if (isSecondLevelDomain(baseDomain) && type == "cname") { + // many providers dont allow cname for this. Lets prevent it for the user for now + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "You cannot create a CNAME record on a root domain. RFC 1912 § 2.4 prohibits CNAME records at the zone apex. Please use a subdomain." + ) + ); + }*/ + let numOrgDomains: OrgDomains[] | undefined; let aRecords: CreateDomainResponse["aRecords"]; let cnameRecords: CreateDomainResponse["cnameRecords"]; @@ -156,7 +155,7 @@ export async function createOrgDomain( .from(domains) .where(eq(domains.verified, true)); - if (type == "cname") { + /* if (type == "cname") { // Block if a verified CNAME exists at the same name const cnameExists = verifiedDomains.some( (d) => d.type === "cname" && d.baseDomain === baseDomain @@ -203,6 +202,7 @@ export async function createOrgDomain( } else if (type == "wildcard") { // TODO: Figure out how to handle wildcards } + */ const domainId = generateId(15); @@ -212,7 +212,7 @@ export async function createOrgDomain( domainId, baseDomain, type, - verified: build == "oss" ? true : false + verified: type === "wildcard" ? true : false }) .returning(); @@ -228,7 +228,7 @@ export async function createOrgDomain( .returning(); // TODO: This needs to be cross region and not hardcoded - if (type === "ns") { + /* if (type === "ns") { nsRecords = config.getRawConfig().dns.nameservers as string[]; } else if (type === "cname") { cnameRecords = [ @@ -241,7 +241,8 @@ export async function createOrgDomain( baseDomain: `_acme-challenge.${baseDomain}` } ]; - } else if (type === "wildcard") { + } */ + if (type === "wildcard") { aRecords = [ { value: `Server IP Address`, diff --git a/server/routers/domain/types.ts b/server/routers/domain/types.ts new file mode 100644 index 00000000..4ae48fb1 --- /dev/null +++ b/server/routers/domain/types.ts @@ -0,0 +1,8 @@ +export type CheckDomainAvailabilityResponse = { + available: boolean; + options: { + domainNamespaceId: string; + domainId: string; + fullDomain: string; + }[]; +}; \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index c48a41a7..c161bef5 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -36,13 +36,12 @@ import { verifyUserIsOrgOwner, verifySiteResourceAccess } from "@server/middlewares"; -import { createStore } from "@server/lib/rateLimitStore"; import { ActionsEnum } from "@server/auth/actions"; import { createNewt, getNewtToken } from "./newt"; import { getOlmToken } from "./olm"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import createHttpError from "http-errors"; -import { build } from "@server/build"; +import { createStore } from "@server/lib/rateLimitStore"; // Root routes export const unauthenticated = Router(); @@ -57,9 +56,7 @@ authenticated.use(verifySessionUserMiddleware); authenticated.get("/pick-org-defaults", org.pickOrgDefaults); authenticated.get("/org/checkId", org.checkId); -if (build === "oss" || build === "enterprise") { - authenticated.put("/org", getUserOrgs, org.createOrg); -} +authenticated.put("/org", getUserOrgs, org.createOrg); authenticated.get("/orgs", verifyUserIsServerAdmin, org.listOrgs); authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs); @@ -77,15 +74,13 @@ authenticated.post( org.updateOrg ); -if (build !== "saas") { - authenticated.delete( - "/org/:orgId", - verifyOrgAccess, - verifyUserIsOrgOwner, - verifyUserHasAction(ActionsEnum.deleteOrg), - org.deleteOrg - ); -} +authenticated.delete( + "/org/:orgId", + verifyOrgAccess, + verifyUserIsOrgOwner, + verifyUserHasAction(ActionsEnum.deleteOrg), + org.deleteOrg +); authenticated.put( "/org/:orgId/site", @@ -496,6 +491,13 @@ authenticated.post( resource.setResourcePincode ); +authenticated.post( + `/resource/:resourceId/header-auth`, + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), + resource.setResourceHeaderAuth +); + authenticated.post( `/resource/:resourceId/whitelist`, verifyResourceAccess, @@ -540,7 +542,10 @@ authenticated.get( authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); -unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); +unauthenticated.get( + "/resource/:resourceGuid/auth", + resource.getResourceAuthInfo +); // authenticated.get( // "/role/:roleId/resources", @@ -666,6 +671,8 @@ authenticated.post( idp.updateOidcIdp ); + + authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -694,6 +701,7 @@ authenticated.get( idp.listIdpOrgPolicies ); + authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -809,7 +817,8 @@ authRouter.use( rateLimit({ windowMs: config.getRawConfig().rate_limits.auth.window_minutes, max: config.getRawConfig().rate_limits.auth.max_requests, - keyGenerator: (req) => `authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`, + keyGenerator: (req) => + `authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`, handler: (req, res, next) => { const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.auth.max_requests} requests every ${config.getRawConfig().rate_limits.auth.window_minutes} minute(s).`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -823,7 +832,8 @@ authRouter.put( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `signup:${ipKeyGenerator(req.ip || "")}:${req.body.email}`, + keyGenerator: (req) => + `signup:${ipKeyGenerator(req.ip || "")}:${req.body.email}`, handler: (req, res, next) => { const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -837,7 +847,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `login:${req.body.email || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `login:${req.body.email || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only log in ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -852,7 +863,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 900, - keyGenerator: (req) => `newtGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `newtGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only request a Newt token ${900} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -866,7 +878,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 900, - keyGenerator: (req) => `olmGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `olmGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only request an Olm token ${900} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -914,7 +927,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `signup:${req.user?.userId || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `signup:${req.user?.userId || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only disable 2FA ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -928,7 +942,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `signup:${req.body.email || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `signup:${req.body.email || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -983,7 +998,8 @@ authRouter.post( rateLimit({ windowMs: 15 * 60 * 1000, max: 15, - keyGenerator: (req) => `resetPassword:${req.body.email || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `resetPassword:${req.body.email || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); @@ -1105,7 +1121,8 @@ authRouter.delete( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 20, // Allow 10 authentication attempts per 15 minutes per IP - keyGenerator: (req) => `securityKeyAuth:${req.user?.userId || ipKeyGenerator(req.ip || "")}`, + keyGenerator: (req) => + `securityKeyAuth:${req.user?.userId || ipKeyGenerator(req.ip || "")}`, handler: (req, res, next) => { const message = `You can only delete a security key ${10} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts new file mode 100644 index 00000000..4c5efed7 --- /dev/null +++ b/server/routers/generatedLicense/types.ts @@ -0,0 +1,30 @@ +export type GeneratedLicenseKey = { + instanceName: string | null; + licenseKey: string; + expiresAt: string; + isValid: boolean; + createdAt: string; + tier: string; + type: string; +}; + +export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[]; + +export type NewLicenseKey = { + licenseKey: { + id: number; + instanceName: string | null; + instanceId: string; + licenseKey: string; + tier: string; + type: string; + quantity: number; + isValid: boolean; + updatedAt: string; + createdAt: string; + expiresAt: string; + orgId: string; + }; +}; + +export type GenerateNewLicenseResponse = NewLicenseKey; \ No newline at end of file diff --git a/server/routers/gerbil/createExitNode.ts b/server/routers/gerbil/createExitNode.ts index d4e6d43a..06af7e46 100644 --- a/server/routers/gerbil/createExitNode.ts +++ b/server/routers/gerbil/createExitNode.ts @@ -30,6 +30,7 @@ export async function createExitNode(publicKey: string, reachableAt: string | un publicKey, endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`, address, + online: true, listenPort, reachableAt, name: exitNodeName @@ -51,7 +52,7 @@ export async function createExitNode(publicKey: string, reachableAt: string | un .where(eq(exitNodes.publicKey, publicKey)) .returning(); - logger.info(`Updated exit node`); + logger.info(`Updated exit node with reachableAt to ${reachableAt}`); } return exitNode; diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 71d1a45e..9c6f2652 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -1,19 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { sites, resources, targets, exitNodes, ExitNode } from "@server/db"; +import { sites, exitNodes, ExitNode } from "@server/db"; import { db } from "@server/db"; import { eq, isNotNull, and } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import config from "@server/lib/config"; -import { getUniqueExitNodeEndpointName } from "../../db/names"; -import { findNextAvailableCidr } from "@server/lib/ip"; import { fromError } from "zod-validation-error"; import { getAllowedIps } from "../target/helpers"; -import { proxyToRemote } from "@server/lib/remoteProxy"; -import { getNextAvailableSubnet } from "@server/lib/exitNodes"; -import { createExitNode } from "./createExitNode"; +import { createExitNode } from "@server/routers/gerbil/createExitNode"; // Define Zod schema for request validation const getConfigSchema = z.object({ @@ -66,16 +62,6 @@ export async function getConfig( ); } - // STOP HERE IN HYBRID MODE - if (config.isManagedMode()) { - req.body = { - ...req.body, - endpoint: exitNode.endpoint, - listenPort: exitNode.listenPort - }; - return proxyToRemote(req, res, next, "hybrid/gerbil/get-config"); - } - const configResponse = await generateGerbilConfig(exitNode); logger.debug("Sending config: ", configResponse); @@ -132,26 +118,3 @@ export async function generateGerbilConfig(exitNode: ExitNode) { return configResponse; } - -async function getNextAvailablePort(): Promise { - // Get all existing ports from exitNodes table - const existingPorts = await db - .select({ - listenPort: exitNodes.listenPort - }) - .from(exitNodes); - - // Find the first available port between 1024 and 65535 - let nextPort = config.getRawConfig().gerbil.start_port; - for (const port of existingPorts) { - if (port.listenPort > nextPort) { - break; - } - nextPort++; - if (nextPort > 65535) { - throw new Error("No available ports remaining in space"); - } - } - - return nextPort; -} diff --git a/server/routers/gerbil/getResolvedHostname.ts b/server/routers/gerbil/getResolvedHostname.ts index da2ab39a..cd0e0b55 100644 --- a/server/routers/gerbil/getResolvedHostname.ts +++ b/server/routers/gerbil/getResolvedHostname.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; // Define Zod schema for request validation const getResolvedHostnameSchema = z.object({ @@ -17,22 +16,11 @@ export async function getResolvedHostname( next: NextFunction ): Promise { try { - // Validate request parameters - const parsedParams = getResolvedHostnameSchema.safeParse( - req.body - ); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } + let endpoints: string[] = []; // always route locally // return the endpoints return res.status(HttpCode.OK).send({ - endpoints: [] // ALWAYS ROUTE LOCALLY + endpoints }); } catch (error) { logger.error(error); diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index 51a338a7..1cdc9184 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -2,7 +2,7 @@ import logger from "@server/logger"; import { db } from "@server/db"; import { exitNodes } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToExitNode } from "../../lib/exitNodeComms"; +import { sendToExitNode } from "@server/lib/exitNodes"; export async function addPeer( exitNodeId: number, diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index fb7723ee..ca878633 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -29,7 +29,7 @@ export const receiveBandwidth = async ( throw new Error("Invalid bandwidth data"); } - await updateSiteBandwidth(bandwidthData); + await updateSiteBandwidth(bandwidthData); // we are checking the usage on saas only return response(res, { data: {}, @@ -89,18 +89,23 @@ export async function updateSiteBandwidth( lastBandwidthUpdate: sites.lastBandwidthUpdate }); - if (exitNodeId) { - if (await checkExitNodeOrg(exitNodeId, updatedSite.orgId)) { - // not allowed - logger.warn( - `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` - ); - // THIS SHOULD TRIGGER THE TRANSACTION TO FAIL? - throw new Error("Exit node not allowed"); - } - } - if (updatedSite) { + if (exitNodeId) { + if ( + await checkExitNodeOrg( + exitNodeId, + updatedSite.orgId + ) + ) { + // not allowed + logger.warn( + `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` + ); + // THIS SHOULD TRIGGER THE TRANSACTION TO FAIL? + throw new Error("Exit node not allowed"); + } + } + updatedSites.push({ ...updatedSite, peer }); } } @@ -161,7 +166,7 @@ export async function updateSiteBandwidth( .where(eq(sites.siteId, site.siteId)) .returning(); - if (exitNodeId) { + if (updatedSite && exitNodeId) { if ( await checkExitNodeOrg( exitNodeId, diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 1662e420..b5885314 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -18,7 +18,6 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; import { validateOlmSessionToken } from "@server/auth/sessions/olm"; -import axios from "axios"; import { checkExitNodeOrg } from "@server/lib/exitNodes"; // Define Zod schema for request validation @@ -105,7 +104,7 @@ export async function updateHolePunch( destinations: destinations }); } catch (error) { - logger.error(error); + // logger.error(error); // FIX THIS return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, @@ -122,7 +121,8 @@ export async function updateAndGenerateEndpointDestinations( port: number, timestamp: number, token: string, - exitNode: ExitNode + exitNode: ExitNode, + checkOrg = false ) { let currentSiteId: number | undefined; const destinations: PeerDestination[] = []; @@ -158,7 +158,7 @@ export async function updateAndGenerateEndpointDestinations( .where(eq(clients.clientId, olm.clientId)) .returning(); - if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId)) { + if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId) && checkOrg) { // not allowed logger.warn( `Exit node ${exitNode.exitNodeId} is not allowed for org ${client.orgId}` @@ -253,7 +253,7 @@ export async function updateAndGenerateEndpointDestinations( .where(eq(sites.siteId, newt.siteId)) .limit(1); - if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId)) { + if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId) && checkOrg) { // not allowed logger.warn( `Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}` diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index e833d966..67357d76 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -111,7 +111,7 @@ export async function createOidcIdp( }); }); - const redirectUrl = generateOidcRedirectUrl(idpId as number); + const redirectUrl = await generateOidcRedirectUrl(idpId as number); return response(res, { data: { diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index e862c81c..58b231b7 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -12,6 +12,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z .object({ + orgId: z.string().optional(), // Optional; used with org idp in saas idpId: z.coerce.number() }) .strict(); diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index c507198a..242dfa37 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -10,7 +10,6 @@ import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; -import cookie from "cookie"; import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; import { decrypt } from "@server/lib/crypto"; @@ -27,6 +26,10 @@ const bodySchema = z }) .strict(); +const querySchema = z.object({ + orgId: z.string().optional() // check what actuall calls it +}); + const ensureTrailingSlash = (url: string): string => { return url; }; @@ -65,6 +68,18 @@ export async function generateOidcUrl( const { redirectUrl: postAuthRedirectUrl } = parsedBody.data; + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedQuery.data; + const [existingIdp] = await db .select() .from(idp) @@ -80,6 +95,23 @@ export async function generateOidcUrl( ); } + if (orgId) { + const [idpOrgLink] = await db + .select() + .from(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))) + .limit(1); + + if (!idpOrgLink) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IdP not found for the organization" + ) + ); + } + } + const parsedScopes = existingIdp.idpOidcConfig.scopes .split(" ") .map((scope) => { @@ -100,7 +132,12 @@ export async function generateOidcUrl( key ); - const redirectUrl = generateOidcRedirectUrl(idpId); + const redirectUrl = await generateOidcRedirectUrl(idpId, orgId); + logger.debug("OIDC client info", { + decryptedClientId, + decryptedClientSecret, + redirectUrl + }); const client = new arctic.OAuth2Client( decryptedClientId, decryptedClientSecret, @@ -116,7 +153,6 @@ export async function generateOidcUrl( codeVerifier, parsedScopes ); - logger.debug("Generated OIDC URL", { url }); const stateJwt = jsonwebtoken.sign( { diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index 37491388..f0dcf02e 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -7,5 +7,5 @@ export * from "./validateOidcCallback"; export * from "./getIdp"; export * from "./createIdpOrgPolicy"; export * from "./deleteIdpOrgPolicy"; -export * from "./updateIdpOrgPolicy"; export * from "./listIdpOrgPolicies"; +export * from "./updateIdpOrgPolicy"; diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts index 3571b59d..8bde3c0c 100644 --- a/server/routers/idp/oidcAutoProvision.ts +++ b/server/routers/idp/oidcAutoProvision.ts @@ -20,6 +20,8 @@ import { import { eq, and, inArray } from "drizzle-orm"; import jmespath from "jmespath"; import { UserType } from "@server/types/UserTypes"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; const extractedRolesSchema = z.array(z.string()).or(z.string()).nullable(); @@ -123,6 +125,22 @@ export async function oidcAutoProvision({ } logger.debug("User org info", { userOrgInfo }); + if (!userOrgInfo.length) { + if (existingUser) { + // delete the user + // cascade will also delete org users + + await db + .delete(users) + .where(eq(users.userId, existingUser.userId)); + } + + throw createHttpError( + HttpCode.UNAUTHORIZED, + `No policies matched for ${userIdentifier}. This user must be added to an organization before logging in.` + ); + } + let userId = existingUser?.userId; // sync the user with the orgs and roles await db.transaction(async (trx) => { diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 973a1b9a..dd7331bd 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,9 +1,9 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, Org } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; +import createHttpError, { HttpError } from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { @@ -41,6 +41,10 @@ const bodySchema = z.object({ storedState: z.string().nonempty() }); +const querySchema = z.object({ + loginPageId: z.coerce.number().optional() +}); + export type ValidateOidcUrlCallbackResponse = { redirectUrl: string; }; @@ -73,6 +77,18 @@ export async function validateOidcCallback( ); } + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { loginPageId } = parsedQuery.data; + const { storedState, code, state: expectedState } = parsedBody.data; const [existingIdp] = await db @@ -101,7 +117,11 @@ export async function validateOidcCallback( key ); - const redirectUrl = generateOidcRedirectUrl(existingIdp.idp.idpId); + const redirectUrl = await generateOidcRedirectUrl( + existingIdp.idp.idpId, + undefined, + loginPageId + ); const client = new arctic.OAuth2Client( decryptedClientId, decryptedClientSecret, @@ -227,16 +247,21 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - await oidcAutoProvision({ - idp: existingIdp.idp, - userIdentifier, - email, - name, - claims, - existingUser, - req, - res - }); + try { + await oidcAutoProvision({ + idp: existingIdp.idp, + userIdentifier, + email, + name, + claims, + existingUser, + req, + res + }); + } catch (e) { + if (e instanceof HttpError) return next(e); + else throw e; + } return response(res, { data: { diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 6a43aaa7..3513cc64 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -24,8 +24,7 @@ import { verifyApiKeyIsRoot, verifyApiKeyClientAccess, verifyClientsEnabled, - verifyApiKeySiteResourceAccess, - verifyOrgAccess + verifyApiKeySiteResourceAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -400,6 +399,13 @@ authenticated.post( resource.setResourcePincode ); +authenticated.post( + `/resource/:resourceId/header-auth`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth), + resource.setResourceHeaderAuth +); + authenticated.post( `/resource/:resourceId/whitelist`, verifyApiKeyResourceAccess, @@ -548,13 +554,6 @@ authenticated.post( idp.updateOidcIdp ); -authenticated.delete( - "/idp/:idpId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteIdp), - idp.deleteIdp -); - authenticated.get( "/idp", verifyApiKeyIsRoot, @@ -650,4 +649,4 @@ authenticated.put( verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.applyBlueprint), org.applyBlueprint -); \ No newline at end of file +); diff --git a/server/routers/internal.ts b/server/routers/internal.ts index dbccd5b4..dc40ea27 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -5,8 +5,6 @@ import * as resource from "./resource"; import * as badger from "./badger"; import * as auth from "@server/routers/auth"; import * as idp from "@server/routers/idp"; -import { proxyToRemote } from "@server/lib/remoteProxy"; -import config from "@server/lib/config"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, @@ -14,7 +12,7 @@ import { } from "@server/middlewares"; // Root routes -const internalRouter = Router(); +export const internalRouter = Router(); internalRouter.get("/", (_, res) => { res.status(HttpCode.OK).json({ message: "Healthy" }); @@ -42,34 +40,11 @@ internalRouter.get("/idp/:idpId", idp.getIdp); const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -if (config.isManagedMode()) { - // Use proxy router to forward requests to remote cloud server - // Proxy endpoints for each gerbil route - gerbilRouter.post("/receive-bandwidth", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth") - ); - - gerbilRouter.post("/update-hole-punch", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch") - ); - - gerbilRouter.post("/get-all-relays", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays") - ); - - gerbilRouter.post("/get-resolved-hostname", (req, res, next) => - proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`) - ); - - // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER - // SO IT CAN REGISTER THE LOCAL EXIT NODE -} else { - // Use local gerbil endpoints - gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); - gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); - gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); - gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname); -} +// Use local gerbil endpoints +gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); +gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch); +gerbilRouter.post("/get-all-relays", gerbil.getAllRelays); +gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname); // WE HANDLE THE PROXY INSIDE OF THIS FUNCTION // SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL @@ -81,12 +56,4 @@ internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); -if (config.isManagedMode()) { - badgerRouter.post("/exchange-session", (req, res, next) => - proxyToRemote(req, res, next, "hybrid/badger/exchange-session") - ); -} else { - badgerRouter.post("/exchange-session", badger.exchangeSession); -} - -export default internalRouter; +badgerRouter.post("/exchange-session", badger.exchangeSession); diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts new file mode 100644 index 00000000..26f59cab --- /dev/null +++ b/server/routers/loginPage/types.ts @@ -0,0 +1,11 @@ +import { LoginPage } from "@server/db"; + +export type CreateLoginPageResponse = LoginPage; + +export type DeleteLoginPageResponse = LoginPage; + +export type GetLoginPageResponse = LoginPage; + +export type UpdateLoginPageResponse = LoginPage; + +export type LoadLoginPageResponse = LoginPage & { orgId: string }; \ No newline at end of file diff --git a/server/routers/newt/dockerSocket.ts b/server/routers/newt/dockerSocket.ts index 0c59d354..071069fe 100644 --- a/server/routers/newt/dockerSocket.ts +++ b/server/routers/newt/dockerSocket.ts @@ -1,5 +1,5 @@ import NodeCache from "node-cache"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; export const dockerSocketCache = new NodeCache({ stdTTL: 3600 // seconds diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts index 68158799..62802fff 100644 --- a/server/routers/newt/handleApplyBlueprintMessage.ts +++ b/server/routers/newt/handleApplyBlueprintMessage.ts @@ -1,5 +1,5 @@ import { db, newts } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db"; import { eq, and, sql, inArray } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index b6206064..e08e4132 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { @@ -14,7 +14,7 @@ import { import { clients, clientSites, Newt, sites } from "@server/db"; import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; -import { sendToExitNode } from "../../lib/exitNodeComms"; +import { sendToExitNode } from "@server/lib/exitNodes"; const inputSchema = z.object({ publicKey: z.string(), @@ -66,7 +66,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // we need to wait for hole punch success if (!existingSite.endpoint) { - logger.warn(`Site ${existingSite.siteId} has no endpoint, skipping`); + logger.debug(`In newt get config: existing site ${existingSite.siteId} has no endpoint, skipping`); return; } @@ -74,12 +74,12 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly) } - if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 6) { - logger.warn( - `Site ${existingSite.siteId} last hole punch is too old, skipping` - ); - return; - } + // if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 6) { + // logger.warn( + // `Site ${existingSite.siteId} last hole punch is too old, skipping` + // ); + // return; + // } // update the endpoint and the public key const [site] = await db @@ -176,7 +176,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { if (!endpoint) { logger.warn( - `Site ${site.siteId} has no endpoint, skipping` + `In Newt get config: Peer site ${site.siteId} has no endpoint, skipping` ); return null; } diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts index f93862f6..8b28c2a8 100644 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ b/server/routers/newt/handleNewtPingRequestMessage.ts @@ -1,8 +1,7 @@ import { db, sites } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt } from "@server/db"; import logger from "@server/logger"; -import config from "@server/lib/config"; import { ne, eq, or, and, count } from "drizzle-orm"; import { listExitNodes } from "@server/lib/exitNodes"; @@ -29,7 +28,14 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { .where(eq(sites.siteId, newt.siteId)) .limit(1); - const exitNodesList = await listExitNodes(site.orgId, true); // filter for only the online ones + if (!site || !site.orgId) { + logger.warn("Site not found"); + return; + } + + const { noCloud } = message.data; + + 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 eef78765..3df7822d 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -1,6 +1,7 @@ -import { db, newts } from "@server/db"; -import { MessageHandler } from "../ws"; +import { db, exitNodeOrgs, newts } from "@server/db"; +import { MessageHandler } from "@server/routers/ws"; import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db"; +import { targetHealthCheck } from "@server/db"; import { eq, and, sql, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../gerbil/peers"; import logger from "@server/logger"; @@ -9,7 +10,10 @@ import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; -import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; +import { + selectBestExitNode, + verifyExitNodeOrgAccess +} from "@server/lib/exitNodes"; import { fetchContainers } from "./dockerSocket"; export type ExitNodePingResult = { @@ -22,6 +26,8 @@ export type ExitNodePingResult = { wasPreviouslyConnected: boolean; }; +const numTimesLimitExceededForId: Record = {}; + export const handleNewtRegisterMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; const newt = client as Newt; @@ -92,7 +98,10 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { // This effectively moves the exit node to the new one exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId - const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(exitNodeIdToQuery, oldSite.orgId); + const { exitNode, hasAccess } = await verifyExitNodeOrgAccess( + exitNodeIdToQuery, + oldSite.orgId + ); if (!exitNode) { logger.warn("Exit node not found"); @@ -114,6 +123,10 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { const blockSize = config.getRawConfig().gerbil.site_block_size; 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 !== null); subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`)); const newSubnet = findNextAvailableCidr( @@ -122,7 +135,9 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { exitNode.address ); if (!newSubnet) { - logger.error("No available subnets found for the new exit node"); + logger.error( + `No available subnets found for the new exit node id ${exitNodeId} and site id ${siteId}` + ); return; } @@ -168,11 +183,25 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { return; } - // add the peer to the exit node - await addPeer(exitNodeIdToQuery, { - publicKey: publicKey, - allowedIps: [siteSubnet] - }); + try { + // add the peer to the exit node + await addPeer(exitNodeIdToQuery, { + publicKey: publicKey, + allowedIps: [siteSubnet] + }); + } catch (error) { + logger.error(`Failed to add peer to exit node: ${error}`); + } + + if (newtVersion && newtVersion !== newt.version) { + // update the newt version in the database + await db + .update(newts) + .set({ + version: newtVersion as string + }) + .where(eq(newts.newtId, newt.newtId)); + } if (newtVersion && newtVersion !== newt.version) { // update the newt version in the database @@ -194,10 +223,25 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { port: targets.port, internalPort: targets.internalPort, enabled: targets.enabled, - protocol: resources.protocol + protocol: resources.protocol, + hcEnabled: targetHealthCheck.hcEnabled, + hcPath: targetHealthCheck.hcPath, + hcScheme: targetHealthCheck.hcScheme, + hcMode: targetHealthCheck.hcMode, + hcHostname: targetHealthCheck.hcHostname, + hcPort: targetHealthCheck.hcPort, + hcInterval: targetHealthCheck.hcInterval, + hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval, + hcTimeout: targetHealthCheck.hcTimeout, + hcHeaders: targetHealthCheck.hcHeaders, + hcMethod: targetHealthCheck.hcMethod }) .from(targets) .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .leftJoin( + targetHealthCheck, + eq(targets.targetId, targetHealthCheck.targetId) + ) .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); const { tcpTargets, udpTargets } = allTargets.reduce( @@ -222,6 +266,59 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { { tcpTargets: [] as string[], udpTargets: [] as string[] } ); + const healthCheckTargets = allTargets.map((target) => { + // make sure the stuff is defined + if ( + !target.hcPath || + !target.hcHostname || + !target.hcPort || + !target.hcInterval || + !target.hcMethod + ) { + logger.debug( + `Skipping target ${target.targetId} due to missing health check fields` + ); + return null; // Skip targets with missing health check fields + } + + // parse headers + const hcHeadersParse = target.hcHeaders + ? JSON.parse(target.hcHeaders) + : null; + const hcHeadersSend: { [key: string]: string } = {}; + if (hcHeadersParse) { + hcHeadersParse.forEach( + (header: { name: string; value: string }) => { + hcHeadersSend[header.name] = header.value; + } + ); + } + + return { + id: target.targetId, + hcEnabled: target.hcEnabled, + hcPath: target.hcPath, + hcScheme: target.hcScheme, + hcMode: target.hcMode, + hcHostname: target.hcHostname, + hcPort: target.hcPort, + hcInterval: target.hcInterval, // in seconds + hcUnhealthyInterval: target.hcUnhealthyInterval, // in seconds + hcTimeout: target.hcTimeout, // in seconds + hcHeaders: hcHeadersSend, + hcMethod: target.hcMethod + }; + }); + + // Filter out any null values from health check targets + const validHealthCheckTargets = healthCheckTargets.filter( + (target) => target !== null + ); + + logger.debug( + `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` + ); + return { message: { type: "newt/wg/connect", @@ -233,10 +330,11 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { targets: { udp: udpTargets, tcp: tcpTargets - } + }, + healthCheckTargets: validHealthCheckTargets } }, broadcast: false, // Send to all clients excludeSender: false // Include sender in broadcast }; -}; \ No newline at end of file +}; diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts index 89b24f78..f5170feb 100644 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ b/server/routers/newt/handleReceiveBandwidthMessage.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, Newt } from "@server/db"; import { eq } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index aceca37d..0491393f 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -1,4 +1,4 @@ -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; import { dockerSocketCache } from "./dockerSocket"; import { Newt } from "@server/db"; diff --git a/server/routers/newt/peers.ts b/server/routers/newt/peers.ts index ff57e6fd..0c0765a5 100644 --- a/server/routers/newt/peers.ts +++ b/server/routers/newt/peers.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { newts, sites } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; import logger from "@server/logger"; export async function addPeer( diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 803c3e27..bf8d7290 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,10 +1,12 @@ -import { Target } from "@server/db"; -import { sendToClient } from "../ws"; +import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db"; +import { sendToClient } from "@server/routers/ws"; import logger from "@server/logger"; +import { eq, inArray } from "drizzle-orm"; export async function addTargets( newtId: string, targets: Target[], + healthCheckData: TargetHealthCheck[], protocol: string, port: number | null = null ) { @@ -21,6 +23,62 @@ export async function addTargets( targets: payloadTargets } }); + + // Create a map for quick lookup + const healthCheckMap = new Map(); + 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}`); + 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`); + return null; // Skip targets with missing health check fields + } + + const hcHeadersParse = hc.hcHeaders ? JSON.parse(hc.hcHeaders) : null; + const hcHeadersSend: { [key: string]: string } = {}; + if (hcHeadersParse) { + // transform + hcHeadersParse.forEach((header: { name: string; value: string }) => { + hcHeadersSend[header.name] = header.value; + }); + } + + return { + id: target.targetId, + hcEnabled: hc.hcEnabled, + hcPath: hc.hcPath, + hcScheme: hc.hcScheme, + hcMode: hc.hcMode, + hcHostname: hc.hcHostname, + hcPort: hc.hcPort, + hcInterval: hc.hcInterval, // in seconds + hcUnhealthyInterval: hc.hcUnhealthyInterval, // in seconds + hcTimeout: hc.hcTimeout, // in seconds + hcHeaders: hcHeadersSend, + hcMethod: hc.hcMethod + }; + }); + + // Filter out any null values from health check targets + const validHealthCheckTargets = healthCheckTargets.filter((target) => target !== null); + + await sendToClient(newtId, { + type: `newt/healthcheck/add`, + data: { + targets: validHealthCheckTargets + } + }); } export async function removeTargets( @@ -42,4 +100,15 @@ export async function removeTargets( targets: payloadTargets } }); + + const healthCheckTargets = targets.map((target) => { + return target.targetId; + }); + + await sendToClient(newtId, { + type: `newt/healthcheck/remove`, + data: { + ids: healthCheckTargets + } + }); } diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 6c4b5600..6f00640d 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, Olm } from "@server/db"; import { eq, lt, isNull, and, or } from "drizzle-orm"; import logger from "@server/logger"; diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 11ca8b5e..fdae084d 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,5 +1,5 @@ import { db, ExitNode } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; @@ -88,10 +88,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .where(eq(olms.olmId, olm.olmId)); } - if (now - (client.lastHolePunch || 0) > 6) { - logger.warn("Client last hole punch is too old, skipping all sites"); - return; - } + // if (now - (client.lastHolePunch || 0) > 6) { + // logger.warn("Client last hole punch is too old, skipping all sites"); + // return; + // } if (client.pubKey !== publicKey) { logger.info( @@ -145,7 +145,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Validate endpoint and hole punch status if (!site.endpoint) { - logger.warn(`Site ${site.siteId} has no endpoint, skipping`); + logger.warn(`In olm register: site ${site.siteId} has no endpoint, skipping`); continue; } diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index cefc5b91..9b31754c 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -1,5 +1,5 @@ import { db, exitNodes, sites } from "@server/db"; -import { MessageHandler } from "../ws"; +import { MessageHandler } from "@server/routers/ws"; import { clients, clientSites, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer } from "../newt/peers"; diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index c47c84a8..ab592bdd 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,7 +1,7 @@ import { db } from "@server/db"; import { clients, olms, newts, sites } from "@server/db"; import { eq } from "drizzle-orm"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; import logger from "@server/logger"; export async function addPeer( @@ -24,7 +24,7 @@ export async function addPeer( throw new Error(`Olm with ID ${clientId} not found`); } - sendToClient(olm.olmId, { + await sendToClient(olm.olmId, { type: "olm/wg/peer/add", data: { siteId: peer.siteId, @@ -49,7 +49,7 @@ export async function deletePeer(clientId: number, siteId: number, publicKey: st throw new Error(`Olm with ID ${clientId} not found`); } - sendToClient(olm.olmId, { + await sendToClient(olm.olmId, { type: "olm/wg/peer/remove", data: { publicKey, @@ -80,7 +80,7 @@ export async function updatePeer( throw new Error(`Olm with ID ${clientId} not found`); } - sendToClient(olm.olmId, { + await sendToClient(olm.olmId, { type: "olm/wg/peer/update", data: { siteId: peer.siteId, diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index d26774dd..acfa94a5 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -3,8 +3,6 @@ import { z } from "zod"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import { - apiKeyOrg, - apiKeys, domains, Org, orgDomains, diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index 63e9abb0..a2b0ecd8 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; import { deletePeer } from "../gerbil/peers"; import { OpenAPITags, registry } from "@server/openApi"; diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 35c1a5f7..89c77f13 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -17,7 +17,7 @@ const getOrgSchema = z .strict(); export type GetOrgResponse = { - org: Org; + org: Org & { settings: { } | null }; }; registry.registerPath({ @@ -64,9 +64,23 @@ export async function getOrg( ); } + // Parse settings from JSON string back to object + let parsedSettings = null; + if (org[0].settings) { + try { + parsedSettings = JSON.parse(org[0].settings); + } catch (error) { + // If parsing fails, keep as string for backward compatibility + parsedSettings = org[0].settings; + } + } + return response(res, { data: { - org: org[0] + org: { + ...org[0], + settings: parsedSettings + } }, success: true, error: false, diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index 754def66..7887fcac 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -7,4 +7,4 @@ export * from "./checkId"; export * from "./getOrgOverview"; export * from "./listOrgs"; export * from "./pickOrgDefaults"; -export * from "./applyBlueprint"; \ No newline at end of file +export * from "./applyBlueprint"; diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index ddf31466..0e179eea 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -110,6 +110,7 @@ export async function listUserOrgs( name: orgs.name, subnet: orgs.subnet, createdAt: orgs.createdAt, + settings: orgs.settings, isOwner: sql` exists (select 1 from ${userOrgs} g diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 6dcd1016..6f30e62c 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -12,13 +12,15 @@ import { OpenAPITags, registry } from "@server/openApi"; const updateOrgParamsSchema = z .object({ - orgId: z.string() + orgId: z.string(), }) .strict(); const updateOrgBodySchema = z .object({ - name: z.string().min(1).max(255).optional() + name: z.string().min(1).max(255).optional(), + settings: z.object({ + }).optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -70,11 +72,15 @@ export async function updateOrg( } const { orgId } = parsedParams.data; - const updateData = parsedBody.data; + + const settings = parsedBody.data.settings ? JSON.stringify(parsedBody.data.settings) : undefined; const updatedOrg = await db .update(orgs) - .set(updateData) + .set({ + name: parsedBody.data.name, + settings: settings + }) .where(eq(orgs.orgId, orgId)) .returning(); diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts new file mode 100644 index 00000000..a8e205cc --- /dev/null +++ b/server/routers/orgIdp/types.ts @@ -0,0 +1,27 @@ +import { Idp, IdpOidcConfig } from "@server/db"; + +export type CreateOrgIdpResponse = { + idpId: number; + redirectUrl: string; +}; + +export type GetOrgIdpResponse = { + idp: Idp, + idpOidcConfig: IdpOidcConfig | null, + redirectUrl: string +} + +export type ListOrgIdpsResponse = { + idps: { + idpId: number; + orgId: string; + name: string; + type: string; + variant: string; + }[], + pagination: { + total: number; + limit: number; + offset: number; + }; +}; diff --git a/server/routers/remoteExitNode/types.ts b/server/routers/remoteExitNode/types.ts new file mode 100644 index 00000000..55d0a286 --- /dev/null +++ b/server/routers/remoteExitNode/types.ts @@ -0,0 +1,34 @@ +import { RemoteExitNode } from "@server/db"; + +export type CreateRemoteExitNodeResponse = { + token: string; + remoteExitNodeId: string; + secret: string; +}; + +export type PickRemoteExitNodeDefaultsResponse = { + remoteExitNodeId: string; + secret: string; +}; + +export type QuickStartRemoteExitNodeResponse = { + remoteExitNodeId: string; + secret: string; +}; + +export type ListRemoteExitNodesResponse = { + remoteExitNodes: { + remoteExitNodeId: string; + dateCreated: string; + version: string | null; + exitNodeId: number | null; + name: string; + address: string; + endpoint: string; + online: boolean; + type: string | null; + }[]; + 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 diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index a4311423..759a432f 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -20,7 +20,6 @@ import logger from "@server/logger"; import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; -import { build } from "@server/build"; import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; @@ -36,7 +35,8 @@ const createHttpResourceSchema = z subdomain: z.string().nullable().optional(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string() + domainId: z.string(), + stickySession: z.boolean().optional(), }) .strict() .refine( @@ -54,7 +54,7 @@ const createRawResourceSchema = z name: z.string().min(1).max(255), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().min(1).max(65535), + proxyPort: z.number().int().min(1).max(65535) // enableProxy: z.boolean().default(true) // always true now }) .strict() @@ -142,14 +142,10 @@ export async function createResource( const { http } = req.body; if (http) { - return await createHttpResource( - { req, res, next }, - { orgId } - ); + return await createHttpResource({ req, res, next }, { orgId }); } else { if ( - !config.getRawConfig().flags?.allow_raw_resources && - build == "oss" + !config.getRawConfig().flags?.allow_raw_resources ) { return next( createHttpError( @@ -158,10 +154,7 @@ export async function createResource( ) ); } - return await createRawResource( - { req, res, next }, - { orgId } - ); + return await createRawResource({ req, res, next }, { orgId }); } } catch (error) { logger.error(error); @@ -196,17 +189,13 @@ async function createHttpResource( const { name, domainId } = parsedBody.data; const subdomain = parsedBody.data.subdomain; + const stickySession=parsedBody.data.stickySession; // Validate domain and construct full domain const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain); if (!domainResult.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - domainResult.error - ) - ); + return next(createHttpError(HttpCode.BAD_REQUEST, domainResult.error)); } const { fullDomain, subdomain: finalSubdomain } = domainResult; @@ -244,7 +233,8 @@ async function createHttpResource( subdomain: finalSubdomain, http: true, protocol: "tcp", - ssl: true + ssl: true, + stickySession: stickySession }) .returning(); @@ -285,6 +275,10 @@ async function createHttpResource( ); } + /* if (build != "oss") { + await createCertificate(domainId, fullDomain, db); + }*/ + return response(res, { data: resource, success: true, @@ -332,7 +326,7 @@ async function createRawResource( name, http, protocol, - proxyPort, + proxyPort // enableProxy }) .returning(); diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index affd7625..7cb83d8b 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -18,7 +18,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const createResourceRuleSchema = z .object({ action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH"]), + match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]), value: z.string().min(1), priority: z.number().int(), enabled: z.boolean().optional() diff --git a/server/routers/resource/getExchangeToken.ts b/server/routers/resource/getExchangeToken.ts index ba01f63b..605e5ca6 100644 --- a/server/routers/resource/getExchangeToken.ts +++ b/server/routers/resource/getExchangeToken.ts @@ -14,7 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; const getExchangeTokenParams = z .object({ diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index c775564b..960dda5e 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -1,7 +1,8 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; import { + db, + resourceHeaderAuth, resourcePassword, resourcePincode, resources @@ -15,18 +16,18 @@ import logger from "@server/logger"; const getResourceAuthInfoSchema = z .object({ - resourceId: z - .string() - .transform(Number) - .pipe(z.number().int().positive()) + resourceGuid: z.string() }) .strict(); export type GetResourceAuthInfoResponse = { resourceId: number; + resourceGuid: string; resourceName: string; + niceId: string; password: boolean; pincode: boolean; + headerAuth: boolean; sso: boolean; blockAccess: boolean; url: string; @@ -51,7 +52,9 @@ export async function getResourceAuthInfo( ); } - const { resourceId } = parsedParams.data; + const { resourceGuid } = parsedParams.data; + + const isGuidInteger = /^\d+$/.test(resourceGuid); const [result] = await db .select() @@ -64,27 +67,36 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - .where(eq(resources.resourceId, resourceId)) + + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) + .where(eq(resources.resourceGuid, resourceGuid)) .limit(1); const resource = result?.resources; - const pincode = result?.resourcePincode; - const password = result?.resourcePassword; - - const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; - if (!resource) { return next( createHttpError(HttpCode.NOT_FOUND, "Resource not found") ); } + const pincode = result?.resourcePincode; + const password = result?.resourcePassword; + const headerAuth = result?.resourceHeaderAuth; + + const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; + return response(res, { data: { + niceId: resource.niceId, + resourceGuid: resource.resourceGuid, resourceId: resource.resourceId, resourceName: resource.name, password: password !== null, pincode: pincode !== null, + headerAuth: headerAuth !== null, sso: resource.sso, blockAccess: resource.blockAccess, url, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 1a2e5c2d..60938342 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -22,3 +22,4 @@ export * from "./deleteResourceRule"; export * from "./listResourceRules"; export * from "./updateResourceRule"; export * from "./getUserResources"; +export * from "./setResourceHeaderAuth"; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 45d225ed..eada5e16 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, resourceHeaderAuth } from "@server/db"; import { resources, userResources, @@ -56,7 +56,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { proxyPort: resources.proxyPort, enabled: resources.enabled, domainId: resources.domainId, - niceId: resources.niceId + niceId: resources.niceId, + headerAuthId: resourceHeaderAuth.headerAuthId }) .from(resources) .leftJoin( @@ -67,6 +68,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { resourcePincode, eq(resourcePincode.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) .where( and( inArray(resources.resourceId, accessibleResourceIds), diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts new file mode 100644 index 00000000..dc0d417d --- /dev/null +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -0,0 +1,101 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, resourceHeaderAuth } from "@server/db"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { response } from "@server/lib/response"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourceAuthMethodsParamsSchema = z.object({ + resourceId: z.string().transform(Number).pipe(z.number().int().positive()) +}); + +const setResourceAuthMethodsBodySchema = z + .object({ + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable() + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/resource/{resourceId}/header-auth", + description: + "Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.", + tags: [OpenAPITags.Resource], + request: { + params: setResourceAuthMethodsParamsSchema, + body: { + content: { + "application/json": { + schema: setResourceAuthMethodsBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourceHeaderAuth( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourceAuthMethodsParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourceAuthMethodsBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + const { user, password } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourceHeaderAuth) + .where(eq(resourceHeaderAuth.resourceId, resourceId)); + + if (user && password) { + const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); + + await trx + .insert(resourceHeaderAuth) + .values({ resourceId, headerAuthHash }); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Header Authentication set successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index d1d4a655..5ff485d2 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -7,7 +7,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import { hash } from "@node-rs/argon2"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { OpenAPITags, registry } from "@server/openApi"; diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index d8553c8c..83af3c7a 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -7,7 +7,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import { hash } from "@node-rs/argon2"; -import { response } from "@server/lib"; +import { response } from "@server/lib/response"; import stoi from "@server/lib/stoi"; import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index e70b9496..9aecfaff 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, loginPage } from "@server/db"; import { domains, Org, @@ -21,7 +21,6 @@ import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; -import { validateHeaders } from "@server/lib/validators"; const updateResourceParamsSchema = z .object({ @@ -47,7 +46,10 @@ const updateHttpResourceBodySchema = z tlsServerName: z.string().nullable().optional(), setHostHeader: z.string().nullable().optional(), skipToIdpId: z.number().int().positive().nullable().optional(), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable() + .optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -238,10 +240,7 @@ async function updateHttpResource( if (!domainResult.success) { return next( - createHttpError( - HttpCode.BAD_REQUEST, - domainResult.error - ) + createHttpError(HttpCode.BAD_REQUEST, domainResult.error) ); } @@ -278,6 +277,10 @@ async function updateHttpResource( // Update the subdomain in the update data updateData.subdomain = finalSubdomain; + + /* if (build != "oss") { + await createCertificate(domainId, fullDomain, db); + }*/ } let headers = null; diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index c2b6a47a..06061da9 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -30,7 +30,7 @@ const updateResourceRuleParamsSchema = z const updateResourceRuleSchema = z .object({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), - match: z.enum(["CIDR", "IP", "PATH"]).optional(), + match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]).optional(), value: z.string().min(1).optional(), priority: z.number().int(), enabled: z.boolean().optional() diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 6dda378a..f1a6428d 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -16,7 +16,6 @@ import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; -import config from "@server/lib/config"; import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; const createSiteParamsSchema = z @@ -42,15 +41,15 @@ const createSiteSchema = z address: z.string().optional(), type: z.enum(["newt", "wireguard", "local"]) }) - .strict() - .refine((data) => { - if (data.type === "local") { - return !config.getRawConfig().flags?.disable_local_sites; - } else if (data.type === "wireguard") { - return !config.getRawConfig().flags?.disable_basic_wireguard_sites; - } - return true; - }); + .strict(); +// .refine((data) => { +// if (data.type === "local") { +// return !config.getRawConfig().flags?.disable_local_sites; +// } else if (data.type === "wireguard") { +// return !config.getRawConfig().flags?.disable_basic_wireguard_sites; +// } +// return true; +// }); export type CreateSiteBody = z.infer; diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 4af2feae..5dc68f14 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { deletePeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; import { OpenAPITags, registry } from "@server/openApi"; const deleteSiteSchema = z diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 6f683f7c..e1bb88f6 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,4 +1,4 @@ -import { db, newts } from "@server/db"; +import { db, exitNodes, newts } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; @@ -105,11 +105,15 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { type: sites.type, online: sites.online, address: sites.address, - newtVersion: newts.version + newtVersion: newts.version, + exitNodeId: sites.exitNodeId, + exitNodeName: exitNodes.name, + exitNodeEndpoint: exitNodes.endpoint }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(newts, eq(newts.siteId, sites.siteId)) + .leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId)) .where( and( inArray(sites.siteId, accessibleSiteIds), diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 58d44744..46d3c53b 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -74,6 +74,12 @@ export async function pickSiteDefaults( const randomExitNode = exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; + if (!randomExitNode) { + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "No available exit node") + ); + } + // TODO: this probably can be optimized... // list all of the sites on that exit node const sitesQuery = await db @@ -86,6 +92,7 @@ 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 !== 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 34084a0a..20395641 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -9,7 +9,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; -import { sendToClient } from "../ws"; +import { sendToClient } from "@server/routers/ws"; import { fetchContainers, dockerSocketCache, diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index fb85f566..1aef3251 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, TargetHealthCheck, targetHealthCheck } from "@server/db"; import { newts, resources, sites, Target, targets } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -31,12 +31,43 @@ const createTargetSchema = z method: z.string().optional().nullable(), port: z.number().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.number().int().positive().optional().nullable(), + hcInterval: z.number().int().positive().min(5).optional().nullable(), + hcUnhealthyInterval: z + .number() + .int() + .positive() + .min(5) + .optional() + .nullable(), + hcTimeout: z.number().int().positive().min(1).optional().nullable(), + hcHeaders: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable() + .optional(), + hcFollowRedirects: z.boolean().optional().nullable(), + hcMethod: z.string().min(1).optional().nullable(), + hcStatus: z.number().int().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.number().int().min(1).max(1000).optional().nullable() }) .strict(); -export type CreateTargetResponse = Target; +export type CreateTargetResponse = Target & TargetHealthCheck; registry.registerPath({ method: "put", @@ -141,12 +172,15 @@ export async function createTarget( } let newTarget: Target[] = []; + let healthCheck: TargetHealthCheck[] = []; + let targetIps: string[] = []; if (site.type == "local") { newTarget = await db .insert(targets) .values({ resourceId, - ...targetData + ...targetData, + priority: targetData.priority || 100 }) .returning(); } else { @@ -163,7 +197,10 @@ export async function createTarget( ); } - const { internalPort, targetIps } = await pickPort(site.siteId!, db); + const { internalPort, targetIps: newTargetIps } = await pickPort( + site.siteId!, + db + ); if (!internalPort) { return next( @@ -178,40 +215,81 @@ export async function createTarget( .insert(targets) .values({ resourceId, + siteId: site.siteId, + ip: targetData.ip, + method: targetData.method, + port: targetData.port, internalPort, - ...targetData + enabled: targetData.enabled, + path: targetData.path, + pathMatchType: targetData.pathMatchType, + rewritePath: targetData.rewritePath, + rewritePathType: targetData.rewritePathType, + priority: targetData.priority || 100 }) .returning(); // add the new target to the targetIps array - targetIps.push(`${targetData.ip}/32`); + newTargetIps.push(`${targetData.ip}/32`); - if (site.pubKey) { - if (site.type == "wireguard") { - await addPeer(site.exitNodeId!, { - publicKey: site.pubKey, - allowedIps: targetIps.flat() - }); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); + targetIps = newTargetIps; + } - await addTargets( - newt.newtId, - newTarget, - resource.protocol, - resource.proxyPort - ); - } + let hcHeaders = null; + if (targetData.hcHeaders) { + hcHeaders = JSON.stringify(targetData.hcHeaders); + } + + healthCheck = await db + .insert(targetHealthCheck) + .values({ + targetId: newTarget[0].targetId, + hcEnabled: targetData.hcEnabled ?? false, + hcPath: targetData.hcPath ?? null, + hcScheme: targetData.hcScheme ?? null, + hcMode: targetData.hcMode ?? null, + hcHostname: targetData.hcHostname ?? null, + hcPort: targetData.hcPort ?? null, + hcInterval: targetData.hcInterval ?? null, + hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null, + hcTimeout: targetData.hcTimeout ?? null, + hcHeaders: hcHeaders, + hcFollowRedirects: targetData.hcFollowRedirects ?? null, + hcMethod: targetData.hcMethod ?? null, + hcStatus: targetData.hcStatus ?? null, + hcHealth: "unknown" + }) + .returning(); + + if (site.pubKey) { + if (site.type == "wireguard") { + await addPeer(site.exitNodeId!, { + publicKey: site.pubKey, + allowedIps: targetIps.flat() + }); + } else if (site.type == "newt") { + // get the newt on the site by querying the newt table for siteId + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + await addTargets( + newt.newtId, + newTarget, + healthCheck, + resource.protocol, + resource.proxyPort + ); } } return response(res, { - data: newTarget[0], + data: { + ...newTarget[0], + ...healthCheck[0] + }, success: true, error: false, message: "Target created successfully", diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index b0691087..864c02eb 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Target } from "@server/db"; +import { db, Target, targetHealthCheck, TargetHealthCheck } from "@server/db"; import { targets } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; @@ -16,7 +16,9 @@ const getTargetSchema = z }) .strict(); -type GetTargetResponse = Target; +type GetTargetResponse = Target & Omit & { + hcHeaders: { name: string; value: string; }[] | null; +}; registry.registerPath({ method: "get", @@ -62,8 +64,29 @@ export async function getTarget( ); } + const [targetHc] = await db + .select() + .from(targetHealthCheck) + .where(eq(targetHealthCheck.targetId, targetId)) + .limit(1); + + // Parse hcHeaders from JSON string back to array + let parsedHcHeaders = null; + if (targetHc?.hcHeaders) { + try { + parsedHcHeaders = JSON.parse(targetHc.hcHeaders); + } catch (error) { + // If parsing fails, keep as string for backward compatibility + parsedHcHeaders = targetHc.hcHeaders; + } + } + return response(res, { - data: target[0], + data: { + ...target[0], + ...targetHc, + hcHeaders: parsedHcHeaders + }, success: true, error: false, message: "Target retrieved successfully", diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts new file mode 100644 index 00000000..ee4e7950 --- /dev/null +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -0,0 +1,114 @@ +import { db, targets, resources, sites, targetHealthCheck } from "@server/db"; +import { MessageHandler } from "@server/routers/ws"; +import { Newt } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import logger from "@server/logger"; +import { unknown } from "zod"; + +interface TargetHealthStatus { + status: string; + lastCheck: string; + checkCount: number; + lastError?: string; + config: { + id: string; + hcEnabled: boolean; + hcPath?: string; + hcScheme?: string; + hcMode?: string; + hcHostname?: string; + hcPort?: number; + hcInterval?: number; + hcUnhealthyInterval?: number; + hcTimeout?: number; + hcHeaders?: any; + hcMethod?: string; + }; +} + +interface HealthcheckStatusMessage { + targets: Record; +} + +export const handleHealthcheckStatusMessage: MessageHandler = async (context) => { + const { message, client: c } = context; + const newt = c as Newt; + + logger.info("Handling healthcheck status message"); + + if (!newt) { + logger.warn("Newt not found"); + return; + } + + if (!newt.siteId) { + logger.warn("Newt has no site ID"); + return; + } + + const data = message.data as HealthcheckStatusMessage; + + if (!data.targets) { + logger.warn("No targets data in healthcheck status message"); + return; + } + + try { + let successCount = 0; + let errorCount = 0; + + // 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})` : ''}`); + + // Verify the target belongs to this newt's site before updating + // This prevents unauthorized updates to targets from other sites + const targetIdNum = parseInt(targetId); + if (isNaN(targetIdNum)) { + logger.warn(`Invalid target ID: ${targetId}`); + errorCount++; + continue; + } + + const [targetCheck] = await db + .select({ + targetId: targets.targetId, + siteId: targets.siteId + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .innerJoin(sites, eq(targets.siteId, sites.siteId)) + .where( + and( + eq(targets.targetId, targetIdNum), + eq(sites.siteId, newt.siteId) + ) + ) + .limit(1); + + if (!targetCheck) { + logger.warn(`Target ${targetId} not found or does not belong to site ${newt.siteId}`); + errorCount++; + continue; + } + + // Update the target's health status in the database + await db + .update(targetHealthCheck) + .set({ + hcHealth: healthStatus.status + }) + .where(eq(targetHealthCheck.targetId, targetIdNum)) + .execute(); + + 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`); + } catch (error) { + logger.error("Error processing healthcheck status message:", error); + } + + return; +}; diff --git a/server/routers/target/index.ts b/server/routers/target/index.ts index dc1323f7..7d023bbd 100644 --- a/server/routers/target/index.ts +++ b/server/routers/target/index.ts @@ -3,3 +3,4 @@ export * from "./createTarget"; export * from "./deleteTarget"; export * from "./updateTarget"; export * from "./listTargets"; +export * from "./handleHealthcheckStatusMessage"; diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index ca1159d2..04966f6e 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -1,4 +1,4 @@ -import { db, sites } from "@server/db"; +import { db, sites, targetHealthCheck } from "@server/db"; import { targets } from "@server/db"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -45,18 +45,43 @@ function queryTargets(resourceId: number) { resourceId: targets.resourceId, siteId: targets.siteId, siteType: sites.type, + hcEnabled: targetHealthCheck.hcEnabled, + hcPath: targetHealthCheck.hcPath, + hcScheme: targetHealthCheck.hcScheme, + hcMode: targetHealthCheck.hcMode, + hcHostname: targetHealthCheck.hcHostname, + hcPort: targetHealthCheck.hcPort, + hcInterval: targetHealthCheck.hcInterval, + hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval, + hcTimeout: targetHealthCheck.hcTimeout, + hcHeaders: targetHealthCheck.hcHeaders, + hcFollowRedirects: targetHealthCheck.hcFollowRedirects, + hcMethod: targetHealthCheck.hcMethod, + hcStatus: targetHealthCheck.hcStatus, + hcHealth: targetHealthCheck.hcHealth, path: targets.path, - pathMatchType: targets.pathMatchType + pathMatchType: targets.pathMatchType, + rewritePath: targets.rewritePath, + rewritePathType: targets.rewritePathType, + priority: targets.priority, }) .from(targets) .leftJoin(sites, eq(sites.siteId, targets.siteId)) + .leftJoin( + targetHealthCheck, + eq(targetHealthCheck.targetId, targets.targetId) + ) .where(eq(targets.resourceId, resourceId)); return baseQuery; } +type TargetWithParsedHeaders = Omit>[0], 'hcHeaders'> & { + hcHeaders: { name: string; value: string; }[] | null; +}; + export type ListTargetsResponse = { - targets: Awaited>; + targets: TargetWithParsedHeaders[]; pagination: { total: number; limit: number; offset: number }; }; @@ -111,9 +136,26 @@ export async function listTargets( const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; + // Parse hcHeaders from JSON string back to array for each target + const parsedTargetsList = targetsList.map(target => { + let parsedHcHeaders = null; + if (target.hcHeaders) { + try { + parsedHcHeaders = JSON.parse(target.hcHeaders); + } catch (error) { + // If parsing fails, keep as string for backward compatibility + parsedHcHeaders = target.hcHeaders; + } + } + return { + ...target, + hcHeaders: parsedHcHeaders + }; + }); + return response(res, { data: { - targets: targetsList, + targets: parsedTargetsList, pagination: { total: totalCount, limit, diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 928a1a55..d332609d 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, targetHealthCheck } from "@server/db"; import { newts, resources, sites, targets } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; @@ -13,6 +13,7 @@ import { addTargets } from "../newt/targets"; import { pickPort } from "./helpers"; import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; +import { vs } from "@react-email/components"; const updateTargetParamsSchema = z .object({ @@ -27,8 +28,30 @@ const updateTargetBodySchema = z method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), enabled: z.boolean().optional(), + hcEnabled: z.boolean().optional().nullable(), + hcPath: z.string().min(1).optional().nullable(), + hcScheme: z.string().optional().nullable(), + hcMode: z.string().optional().nullable(), + hcHostname: z.string().optional().nullable(), + hcPort: z.number().int().positive().optional().nullable(), + hcInterval: z.number().int().positive().min(5).optional().nullable(), + hcUnhealthyInterval: z + .number() + .int() + .positive() + .min(5) + .optional() + .nullable(), + hcTimeout: z.number().int().positive().min(1).optional().nullable(), + hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), + hcFollowRedirects: z.boolean().optional().nullable(), + hcMethod: z.string().min(1).optional().nullable(), + hcStatus: z.number().int().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.number().int().min(1).max(1000).optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -169,12 +192,46 @@ export async function updateTarget( const [updatedTarget] = await db .update(targets) .set({ - ...parsedBody.data, - internalPort + siteId: parsedBody.data.siteId, + ip: parsedBody.data.ip, + method: parsedBody.data.method, + port: parsedBody.data.port, + internalPort, + enabled: parsedBody.data.enabled, + path: parsedBody.data.path, + pathMatchType: parsedBody.data.pathMatchType, + priority: parsedBody.data.priority, + rewritePath: parsedBody.data.rewritePath, + rewritePathType: parsedBody.data.rewritePathType }) .where(eq(targets.targetId, targetId)) .returning(); + let hcHeaders = null; + if (parsedBody.data.hcHeaders) { + hcHeaders = JSON.stringify(parsedBody.data.hcHeaders); + } + + const [updatedHc] = await db + .update(targetHealthCheck) + .set({ + hcEnabled: parsedBody.data.hcEnabled || false, + hcPath: parsedBody.data.hcPath, + hcScheme: parsedBody.data.hcScheme, + hcMode: parsedBody.data.hcMode, + hcHostname: parsedBody.data.hcHostname, + hcPort: parsedBody.data.hcPort, + hcInterval: parsedBody.data.hcInterval, + hcUnhealthyInterval: parsedBody.data.hcUnhealthyInterval, + hcTimeout: parsedBody.data.hcTimeout, + hcHeaders: hcHeaders, + hcFollowRedirects: parsedBody.data.hcFollowRedirects, + hcMethod: parsedBody.data.hcMethod, + hcStatus: parsedBody.data.hcStatus + }) + .where(eq(targetHealthCheck.targetId, targetId)) + .returning(); + if (site.pubKey) { if (site.type == "wireguard") { await addPeer(site.exitNodeId!, { @@ -192,13 +249,17 @@ export async function updateTarget( await addTargets( newt.newtId, [updatedTarget], + [updatedHc], resource.protocol, resource.proxyPort ); } } return response(res, { - data: updatedTarget, + data: { + ...updatedTarget, + ...updatedHc + }, success: true, error: false, message: "Target updated successfully", diff --git a/server/routers/traefik/index.ts b/server/routers/traefik/index.ts index 5630028c..6f5bd4f0 100644 --- a/server/routers/traefik/index.ts +++ b/server/routers/traefik/index.ts @@ -1 +1 @@ -export * from "./getTraefikConfig"; \ No newline at end of file +export * from "./traefikConfigProvider"; \ No newline at end of file diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts new file mode 100644 index 00000000..89347932 --- /dev/null +++ b/server/routers/traefik/traefikConfigProvider.ts @@ -0,0 +1,60 @@ +import { Request, Response } from "express"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import config from "@server/lib/config"; +import { getTraefikConfig } from "@server/lib/traefik"; +import { getCurrentExitNodeId } from "@server/lib/exitNodes"; + +const badgerMiddlewareName = "badger"; + +export async function traefikConfigProvider( + _: Request, + res: Response +): Promise { + try { + // First query to get resources with site and org info + // Get the current exit node name from config + const currentExitNodeId = await getCurrentExitNodeId(); + + const traefikConfig = await getTraefikConfig( + currentExitNodeId, + config.getRawConfig().traefik.site_types, + true, // filter out the namespace domains in open source + false // generate the login pages on the cloud and hybrid + ); + + if (traefikConfig?.http?.middlewares) { + // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING + traefikConfig.http.middlewares[badgerMiddlewareName] = { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v1", + `http://${ + config.getRawConfig().server.internal_hostname + }:${config.getRawConfig().server.internal_port}` + ).href, + userSessionCookieName: + config.getRawConfig().server.session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }; + } + + return res.status(HttpCode.OK).json(traefikConfig); + } catch (e) { + logger.error(`Failed to build Traefik config: ${e}`); + return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({ + error: "Failed to build Traefik config" + }); + } +} diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 174600fc..d050a2fe 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -28,10 +28,7 @@ const inviteUserParamsSchema = z const inviteUserBodySchema = z .object({ - email: z - .string() - .toLowerCase() - .email(), + email: z.string().toLowerCase().email(), roleId: z.number(), validHours: z.number().gt(0).lte(168), sendEmail: z.boolean().optional(), @@ -99,7 +96,6 @@ export async function inviteUser( regenerate } = parsedBody.data; - // Check if the organization exists const org = await db .select() diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index dcd8c6f2..960ef4da 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -2,13 +2,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resources, sites, UserOrg } from "@server/db"; import { userOrgs, userResources, users, userSites } from "@server/db"; -import { and, eq, exists } from "drizzle-orm"; +import { and, count, eq, exists } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { UserType } from "@server/types/UserTypes"; const removeUserSchema = z .object({ diff --git a/server/routers/ws/index.ts b/server/routers/ws/index.ts index cf95932c..16440ec9 100644 --- a/server/routers/ws/index.ts +++ b/server/routers/ws/index.ts @@ -1 +1,2 @@ -export * from "./ws"; \ No newline at end of file +export * from "./ws"; +export * from "./types"; \ No newline at end of file diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index 8ca33b8a..cbb023b3 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -13,7 +13,8 @@ import { handleOlmPingMessage, startOlmOfflineChecker } from "../olm"; -import { MessageHandler } from "./ws"; +import { handleHealthcheckStatusMessage } from "../target"; +import { MessageHandler } from "./types"; export const messageHandlers: Record = { "newt/wg/register": handleNewtRegisterMessage, @@ -26,6 +27,7 @@ export const messageHandlers: Record = { "newt/socket/containers": handleDockerContainersMessage, "newt/ping/request": handleNewtPingRequestMessage, "newt/blueprint/apply": handleApplyBlueprintMessage, + "newt/healthcheck/status": handleHealthcheckStatusMessage, }; -startOlmOfflineChecker(); // this is to handle the offline check for olms +startOlmOfflineChecker(); // this is to handle the offline check for olms \ No newline at end of file diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts new file mode 100644 index 00000000..7063bc87 --- /dev/null +++ b/server/routers/ws/types.ts @@ -0,0 +1,70 @@ +import { + Newt, + newts, + NewtSession, + olms, + Olm, + OlmSession, + RemoteExitNode, + RemoteExitNodeSession, + remoteExitNodes +} from "@server/db"; +import { IncomingMessage } from "http"; +import { WebSocket } from "ws"; + +// Custom interfaces +export interface WebSocketRequest extends IncomingMessage { + token?: string; +} + +export type ClientType = "newt" | "olm" | "remoteExitNode"; + +export interface AuthenticatedWebSocket extends WebSocket { + client?: Newt | Olm | RemoteExitNode; + clientType?: ClientType; + connectionId?: string; + isFullyConnected?: boolean; + pendingMessages?: Buffer[]; +} + +export interface TokenPayload { + client: Newt | Olm | RemoteExitNode; + session: NewtSession | OlmSession | RemoteExitNodeSession; + clientType: ClientType; +} + +export interface WSMessage { + type: string; + data: any; +} + +export interface HandlerResponse { + message: WSMessage; + broadcast?: boolean; + excludeSender?: boolean; + targetClientId?: string; +} + +export interface HandlerContext { + message: WSMessage; + senderWs: WebSocket; + client: Newt | Olm | RemoteExitNode | undefined; + clientType: ClientType; + sendToClient: (clientId: string, message: WSMessage) => Promise; + broadcastToAllExcept: ( + message: WSMessage, + excludeClientId?: string + ) => Promise; + connectedClients: Map; +} + +export type MessageHandler = (context: HandlerContext) => Promise; + +// Redis message type for cross-node communication +export interface RedisMessage { + type: "direct" | "broadcast"; + targetClientId?: string; + 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 8fb773d3..9bba41dc 100644 --- a/server/routers/ws/ws.ts +++ b/server/routers/ws/ws.ts @@ -1,7 +1,6 @@ import { Router, Request, Response } from "express"; import { Server as HttpServer } from "http"; import { WebSocket, WebSocketServer } from "ws"; -import { IncomingMessage } from "http"; import { Socket } from "net"; import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db"; import { eq } from "drizzle-orm"; @@ -11,50 +10,15 @@ 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"; -// Custom interfaces -interface WebSocketRequest extends IncomingMessage { - token?: string; -} - -type ClientType = 'newt' | 'olm'; - -interface AuthenticatedWebSocket extends WebSocket { - client?: Newt | Olm; - clientType?: ClientType; - connectionId?: string; -} - -interface TokenPayload { +// Subset of TokenPayload for public ws.ts (newt and olm only) +interface PublicTokenPayload { client: Newt | Olm; session: NewtSession | OlmSession; - clientType: ClientType; + clientType: "newt" | "olm"; } -interface WSMessage { - type: string; - data: any; -} - -interface HandlerResponse { - message: WSMessage; - broadcast?: boolean; - excludeSender?: boolean; - targetClientId?: string; -} - -interface HandlerContext { - message: WSMessage; - senderWs: WebSocket; - client: Newt | Olm | undefined; - clientType: ClientType; - sendToClient: (clientId: string, message: WSMessage) => Promise; - broadcastToAllExcept: (message: WSMessage, excludeClientId?: string) => Promise; - connectedClients: Map; -} - -export type MessageHandler = (context: HandlerContext) => Promise; - const router: Router = Router(); const wss: WebSocketServer = new WebSocketServer({ noServer: true }); @@ -153,7 +117,7 @@ const getActiveNodes = async (clientType: ClientType, clientId: string): Promise }; // Token verification middleware -const verifyToken = async (token: string, clientType: ClientType): Promise => { +const verifyToken = async (token: string, clientType: ClientType): Promise => { try { if (clientType === 'newt') { @@ -169,7 +133,7 @@ try { return null; } return { client: existingNewt[0], session, clientType }; - } else { + } else if (clientType === 'olm') { const { session, olm } = await validateOlmSessionToken(token); if (!session || !olm) { return null; @@ -183,13 +147,15 @@ try { } return { client: existingOlm[0], session, clientType }; } + + return null; } catch (error) { logger.error("Token verification failed:", error); return null; } }; -const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: ClientType): 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"); @@ -323,10 +289,6 @@ const cleanup = async (): Promise => { } }; -// Handle process termination -process.on('SIGTERM', cleanup); -process.on('SIGINT', cleanup); - export { router, handleWSUpgrade, diff --git a/server/setup/clearStaleData.ts b/server/setup/clearStaleData.ts index 220a64f5..0140b7b3 100644 --- a/server/setup/clearStaleData.ts +++ b/server/setup/clearStaleData.ts @@ -1,4 +1,4 @@ -import { db } from "@server/db"; +import { db, sessionTransferToken } from "@server/db"; import { emailVerificationCodes, newtSessions, @@ -76,4 +76,14 @@ export async function clearStaleData() { } catch (e) { logger.warn("Error clearing expired resourceOtp:", e); } + + try { + await db + .delete(sessionTransferToken) + .where( + lt(sessionTransferToken.expiresAt, new Date().getTime()) + ); + } catch (e) { + logger.warn("Error clearing expired sessionTransferToken:", e); + } } diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 078c99ee..1734b5e6 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -3,7 +3,6 @@ import { eq } from "drizzle-orm"; import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; import moment from "moment"; import logger from "@server/logger"; -import config from "@server/lib/config"; const random: RandomReader = { read(bytes: Uint8Array): void { @@ -23,11 +22,6 @@ function generateId(length: number): string { } export async function ensureSetupToken() { - if (config.isManagedMode()) { - // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID - return; - } - try { // Check if a server admin already exists const [existingAdmin] = await db diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 04779f30..de3785f3 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -11,6 +11,7 @@ import m3 from "./scriptsPg/1.8.0"; import m4 from "./scriptsPg/1.9.0"; import m5 from "./scriptsPg/1.10.0"; import m6 from "./scriptsPg/1.10.2"; +import m7 from "./scriptsPg/1.11.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -23,6 +24,7 @@ const migrations = [ { version: "1.9.0", run: m4 }, { version: "1.10.0", run: m5 }, { version: "1.10.2", run: m6 }, + { version: "1.11.0", run: m7 }, // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 654c2716..b987b833 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -29,6 +29,7 @@ import m24 from "./scriptsSqlite/1.9.0"; import m25 from "./scriptsSqlite/1.10.0"; import m26 from "./scriptsSqlite/1.10.1"; import m27 from "./scriptsSqlite/1.10.2"; +import m28 from "./scriptsSqlite/1.11.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -57,6 +58,7 @@ const migrations = [ { version: "1.10.0", run: m25 }, { version: "1.10.1", run: m26 }, { version: "1.10.2", run: m27 }, + { version: "1.11.0", run: m28 }, // Add new migrations here as they are created ] as const; @@ -134,7 +136,7 @@ async function executeScripts() { const pendingMigrations = lastExecuted .map((m) => m) .sort((a, b) => semver.compare(b.version, a.version)); - const startVersion = pendingMigrations[0]?.version ?? "0.0.0"; + const startVersion = pendingMigrations[0]?.version ?? APP_VERSION; console.log(`Starting migrations from version ${startVersion}`); const migrationsToRun = migrations.filter((migration) => diff --git a/server/setup/scriptsPg/1.11.0.ts b/server/setup/scriptsPg/1.11.0.ts new file mode 100644 index 00000000..13186b4f --- /dev/null +++ b/server/setup/scriptsPg/1.11.0.ts @@ -0,0 +1,392 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { randomUUID } from "crypto"; + +const version = "1.11.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql` + CREATE TABLE "account" ( + "accountId" serial PRIMARY KEY NOT NULL, + "userId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "accountDomains" ( + "accountId" integer NOT NULL, + "domainId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "certificates" ( + "certId" serial PRIMARY KEY NOT NULL, + "domain" varchar(255) NOT NULL, + "domainId" varchar, + "wildcard" boolean DEFAULT false, + "status" varchar(50) DEFAULT 'pending' NOT NULL, + "expiresAt" bigint, + "lastRenewalAttempt" bigint, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL, + "orderId" varchar(500), + "errorMessage" text, + "renewalCount" integer DEFAULT 0, + "certFile" text, + "keyFile" text, + CONSTRAINT "certificates_domain_unique" UNIQUE("domain") + ); + `); + + await db.execute(sql` + CREATE TABLE "customers" ( + "customerId" varchar(255) PRIMARY KEY NOT NULL, + "orgId" varchar(255) NOT NULL, + "email" varchar(255), + "name" varchar(255), + "phone" varchar(50), + "address" text, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "dnsChallenges" ( + "dnsChallengeId" serial PRIMARY KEY NOT NULL, + "domain" varchar(255) NOT NULL, + "token" varchar(255) NOT NULL, + "keyAuthorization" varchar(1000) NOT NULL, + "createdAt" bigint NOT NULL, + "expiresAt" bigint NOT NULL, + "completed" boolean DEFAULT false + ); + `); + + await db.execute(sql` + CREATE TABLE "domainNamespaces" ( + "domainNamespaceId" varchar(255) PRIMARY KEY NOT NULL, + "domainId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "exitNodeOrgs" ( + "exitNodeId" integer NOT NULL, + "orgId" text NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "limits" ( + "limitId" varchar(255) PRIMARY KEY NOT NULL, + "featureId" varchar(255) NOT NULL, + "orgId" varchar NOT NULL, + "value" real, + "description" text + ); + `); + + await db.execute(sql` + CREATE TABLE "loginPage" ( + "loginPageId" serial PRIMARY KEY NOT NULL, + "subdomain" varchar, + "fullDomain" varchar, + "exitNodeId" integer, + "domainId" varchar + ); + `); + + await db.execute(sql` + CREATE TABLE "loginPageOrg" ( + "loginPageId" integer NOT NULL, + "orgId" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "remoteExitNodeSession" ( + "id" varchar PRIMARY KEY NOT NULL, + "remoteExitNodeId" varchar NOT NULL, + "expiresAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "remoteExitNode" ( + "id" varchar PRIMARY KEY NOT NULL, + "secretHash" varchar NOT NULL, + "dateCreated" varchar NOT NULL, + "version" varchar, + "exitNodeId" integer + ); + `); + + await db.execute(sql` + CREATE TABLE "sessionTransferToken" ( + "token" varchar PRIMARY KEY NOT NULL, + "sessionId" varchar NOT NULL, + "encryptedSession" text NOT NULL, + "expiresAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "subscriptionItems" ( + "subscriptionItemId" serial PRIMARY KEY NOT NULL, + "subscriptionId" varchar(255) NOT NULL, + "planId" varchar(255) NOT NULL, + "priceId" varchar(255), + "meterId" varchar(255), + "unitAmount" real, + "tiers" text, + "interval" varchar(50), + "currentPeriodStart" bigint, + "currentPeriodEnd" bigint, + "name" varchar(255) + ); + `); + + await db.execute(sql` + CREATE TABLE "subscriptions" ( + "subscriptionId" varchar(255) PRIMARY KEY NOT NULL, + "customerId" varchar(255) NOT NULL, + "status" varchar(50) DEFAULT 'active' NOT NULL, + "canceledAt" bigint, + "createdAt" bigint NOT NULL, + "updatedAt" bigint, + "billingCycleAnchor" bigint + ); + `); + + await db.execute(sql` + CREATE TABLE "usage" ( + "usageId" varchar(255) PRIMARY KEY NOT NULL, + "featureId" varchar(255) NOT NULL, + "orgId" varchar NOT NULL, + "meterId" varchar(255), + "instantaneousValue" real, + "latestValue" real NOT NULL, + "previousValue" real, + "updatedAt" bigint NOT NULL, + "rolledOverAt" bigint, + "nextRolloverAt" bigint + ); + `); + + await db.execute(sql` + CREATE TABLE "usageNotifications" ( + "notificationId" serial PRIMARY KEY NOT NULL, + "orgId" varchar NOT NULL, + "featureId" varchar(255) NOT NULL, + "limitId" varchar(255) NOT NULL, + "notificationType" varchar(50) NOT NULL, + "sentAt" bigint NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "resourceHeaderAuth" ( + "headerAuthId" serial PRIMARY KEY NOT NULL, + "resourceId" integer NOT NULL, + "headerAuthHash" varchar NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "targetHealthCheck" ( + "targetHealthCheckId" serial PRIMARY KEY NOT NULL, + "targetId" integer NOT NULL, + "hcEnabled" boolean DEFAULT false NOT NULL, + "hcPath" varchar, + "hcScheme" varchar, + "hcMode" varchar DEFAULT 'http', + "hcHostname" varchar, + "hcPort" integer, + "hcInterval" integer DEFAULT 30, + "hcUnhealthyInterval" integer DEFAULT 30, + "hcTimeout" integer DEFAULT 5, + "hcHeaders" varchar, + "hcFollowRedirects" boolean DEFAULT true, + "hcMethod" varchar DEFAULT 'GET', + "hcStatus" integer, + "hcHealth" text DEFAULT 'unknown' + ); + `); + + await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settings" text;`); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "rewritePath" text;` + ); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "rewritePathType" text;` + ); + await db.execute( + sql`ALTER TABLE "targets" ADD COLUMN "priority" integer DEFAULT 100 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_accountId_account_accountId_fk" FOREIGN KEY ("accountId") REFERENCES "public"."account"("accountId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "certificates" ADD CONSTRAINT "certificates_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "customers" ADD CONSTRAINT "customers_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "domainNamespaces" ADD CONSTRAINT "domainNamespaces_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "limits" ADD CONSTRAINT "limits_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_loginPageId_loginPage_loginPageId_fk" FOREIGN KEY ("loginPageId") REFERENCES "public"."loginPage"("loginPageId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "remoteExitNodeSession" ADD CONSTRAINT "remoteExitNodeSession_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "remoteExitNode" ADD CONSTRAINT "remoteExitNode_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "sessionTransferToken" ADD CONSTRAINT "sessionTransferToken_sessionId_session_id_fk" FOREIGN KEY ("sessionId") REFERENCES "public"."session"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "subscriptionItems" ADD CONSTRAINT "subscriptionItems_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_customerId_customers_customerId_fk" FOREIGN KEY ("customerId") REFERENCES "public"."customers"("customerId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "usage" ADD CONSTRAINT "usage_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "usageNotifications" ADD CONSTRAINT "usageNotifications_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "resourceHeaderAuth" ADD CONSTRAINT "resourceHeaderAuth_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "targetHealthCheck" ADD CONSTRAINT "targetHealthCheck_targetId_targets_targetId_fk" FOREIGN KEY ("targetId") REFERENCES "public"."targets"("targetId") ON DELETE cascade ON UPDATE no action;` + ); + + const webauthnCredentialsQuery = await db.execute( + sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"` + ); + + const webauthnCredentials = webauthnCredentialsQuery.rows as { + credentialId: string; + publicKey: string; + userId: string; + signCount: number; + transports: string | null; + name: string | null; + lastUsed: string; + dateCreated: string; + }[]; + + for (const webauthnCredential of webauthnCredentials) { + const newCredentialId = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.credentialId, "base64") + ) + ); + const newPublicKey = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.publicKey, "base64") + ) + ); + + // Delete the old record + await db.execute(sql` + DELETE FROM "webauthnCredentials" + WHERE "credentialId" = ${webauthnCredential.credentialId} + `); + + // Insert the updated record with converted values + await db.execute(sql` + INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated") + VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated}) + `); + } + + // 1. Add the column with placeholder so NOT NULL is satisfied + await db.execute(sql` + ALTER TABLE "resources" + ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER' + `); + + // 2. Fetch every row to backfill UUIDs + const rows = await db.execute( + sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'` + ); + const resources = rows.rows as { resourceId: number }[]; + + for (const r of resources) { + await db.execute(sql` + UPDATE "resources" + SET "resourceGuid" = ${randomUUID()} + WHERE "resourceId" = ${r.resourceId} + `); + } + + // get all of the targets + const targetsQuery = await db.execute( + sql`SELECT "targetId" FROM "targets"` + ); + const targets = targetsQuery.rows as { + targetId: number; + }[]; + + for (const target of targets) { + await db.execute(sql` + INSERT INTO "targetHealthCheck" ("targetId") + VALUES (${target.targetId}) + `); + } + + // 3. Add UNIQUE constraint now that values are filled + await db.execute(sql` + ALTER TABLE "resources" + ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid") + `); + + await db.execute(sql`COMMIT`); + console.log(`Updated credentialId and publicKey`); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to update credentialId and publicKey"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsPg/1.8.0.ts b/server/setup/scriptsPg/1.8.0.ts index 7c0b181b..f3b6c613 100644 --- a/server/setup/scriptsPg/1.8.0.ts +++ b/server/setup/scriptsPg/1.8.0.ts @@ -17,7 +17,7 @@ export default async function migration() { ALTER TABLE "sites" ADD COLUMN "remoteSubnets" text; ALTER TABLE "user" ADD COLUMN "termsAcceptedTimestamp" varchar; ALTER TABLE "user" ADD COLUMN "termsVersion" varchar; - + COMMIT; `); diff --git a/server/setup/scriptsSqlite/1.11.0.ts b/server/setup/scriptsSqlite/1.11.0.ts new file mode 100644 index 00000000..1247eee9 --- /dev/null +++ b/server/setup/scriptsSqlite/1.11.0.ts @@ -0,0 +1,342 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; +import { isoBase64URL } from "@simplewebauthn/server/helpers"; +import { randomUUID } from "crypto"; + +const version = "1.11.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + db.transaction(() => { + + 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(); + + 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(); + + db.prepare(` + CREATE TABLE 'certificates' ( + 'certId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'domain' text NOT NULL, + 'domainId' text, + 'wildcard' integer DEFAULT false, + 'status' text DEFAULT 'pending' NOT NULL, + 'expiresAt' integer, + 'lastRenewalAttempt' integer, + 'createdAt' integer NOT NULL, + 'updatedAt' integer NOT NULL, + 'orderId' text, + 'errorMessage' text, + 'renewalCount' integer DEFAULT 0, + 'certFile' text, + 'keyFile' text, + FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(`CREATE UNIQUE INDEX 'certificates_domain_unique' ON 'certificates' ('domain');`).run(); + + db.prepare(` + CREATE TABLE 'customers' ( + 'customerId' text PRIMARY KEY NOT NULL, + 'orgId' text NOT NULL, + 'email' text, + 'name' text, + 'phone' text, + 'address' text, + 'createdAt' integer NOT NULL, + 'updatedAt' integer NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'dnsChallenges' ( + 'dnsChallengeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'domain' text NOT NULL, + 'token' text NOT NULL, + 'keyAuthorization' text NOT NULL, + 'createdAt' integer NOT NULL, + 'expiresAt' integer NOT NULL, + 'completed' integer DEFAULT false + ); + `).run(); + + 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(); + + 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(); + + db.prepare(` + CREATE TABLE 'loginPage' ( + 'loginPageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'subdomain' text, + 'fullDomain' text, + 'exitNodeId' integer, + 'domainId' text, + 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(); + + 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(); + + 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(); + + db.prepare(` + CREATE TABLE 'remoteExitNode' ( + 'id' text PRIMARY KEY NOT NULL, + 'secretHash' text NOT NULL, + 'dateCreated' text NOT NULL, + 'version' text, + 'exitNodeId' integer, + FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'sessionTransferToken' ( + 'token' text PRIMARY KEY NOT NULL, + 'sessionId' text NOT NULL, + 'encryptedSession' text NOT NULL, + 'expiresAt' integer NOT NULL, + FOREIGN KEY ('sessionId') REFERENCES 'session'('id') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'subscriptionItems' ( + 'subscriptionItemId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'subscriptionId' text NOT NULL, + 'planId' text NOT NULL, + 'priceId' text, + 'meterId' text, + 'unitAmount' real, + 'tiers' text, + 'interval' text, + 'currentPeriodStart' integer, + 'currentPeriodEnd' integer, + 'name' text, + FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'subscriptions' ( + 'subscriptionId' text PRIMARY KEY NOT NULL, + 'customerId' text NOT NULL, + 'status' text DEFAULT 'active' NOT NULL, + 'canceledAt' integer, + 'createdAt' integer NOT NULL, + 'updatedAt' integer, + 'billingCycleAnchor' integer, + FOREIGN KEY ('customerId') REFERENCES 'customers'('customerId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'usage' ( + 'usageId' text PRIMARY KEY NOT NULL, + 'featureId' text NOT NULL, + 'orgId' text NOT NULL, + 'meterId' text, + 'instantaneousValue' real, + 'latestValue' real NOT NULL, + 'previousValue' real, + 'updatedAt' integer NOT NULL, + 'rolledOverAt' integer, + 'nextRolloverAt' integer, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(` + CREATE TABLE 'usageNotifications' ( + 'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'orgId' text NOT NULL, + 'featureId' text NOT NULL, + 'limitId' text NOT NULL, + 'notificationType' text NOT NULL, + 'sentAt' integer NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + 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(); + + db.prepare(` + CREATE TABLE 'targetHealthCheck' ( + 'targetHealthCheckId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'targetId' integer NOT NULL, + 'hcEnabled' integer DEFAULT false NOT NULL, + 'hcPath' text, + 'hcScheme' text, + 'hcMode' text DEFAULT 'http', + 'hcHostname' text, + 'hcPort' integer, + 'hcInterval' integer DEFAULT 30, + 'hcUnhealthyInterval' integer DEFAULT 30, + 'hcTimeout' integer DEFAULT 5, + 'hcHeaders' text, + 'hcFollowRedirects' integer DEFAULT true, + 'hcMethod' text DEFAULT 'GET', + 'hcStatus' integer, + 'hcHealth' text DEFAULT 'unknown', + FOREIGN KEY ('targetId') REFERENCES 'targets'('targetId') ON UPDATE no action ON DELETE cascade + ); + `).run(); + + db.prepare(`DROP TABLE 'limits';`).run(); + + db.prepare(` + CREATE TABLE 'limits' ( + 'limitId' text PRIMARY KEY NOT NULL, + 'featureId' text NOT NULL, + 'orgId' text NOT NULL, + 'value' real, + 'description' text, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + `).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(); + + const webauthnCredentials = db + .prepare( + `SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'` + ) + .all() as { + credentialId: string; + publicKey: string; + userId: string; + signCount: number; + transports: string | null; + name: string | null; + lastUsed: string; + dateCreated: string; + }[]; + + for (const webauthnCredential of webauthnCredentials) { + const newCredentialId = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.credentialId, "base64") + ) + ); + const newPublicKey = isoBase64URL.fromBuffer( + new Uint8Array( + Buffer.from(webauthnCredential.publicKey, "base64") + ) + ); + + // Delete the old record + db.prepare( + `DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?` + ).run(webauthnCredential.credentialId); + + // Insert the updated record with converted values + db.prepare( + `INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + newCredentialId, + newPublicKey, + webauthnCredential.userId, + webauthnCredential.signCount, + webauthnCredential.transports, + webauthnCredential.name, + webauthnCredential.lastUsed, + webauthnCredential.dateCreated + ); + } + + // 1. Add the column (nullable or with placeholder) if it doesn’t exist yet + db.prepare( + `ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';` + ).run(); + + // 2. Select all rows + const resources = db.prepare(`SELECT resourceId FROM resources`).all() as { + resourceId: number; + }[]; + + // 3. Prefill with random UUIDs + const updateStmt = db.prepare( + `UPDATE resources SET resourceGuid = ? WHERE resourceId = ?` + ); + + for (const row of resources) { + updateStmt.run(randomUUID(), row.resourceId); + } + + // get all of the targets + const targets = db.prepare(`SELECT targetId FROM targets`).all() as { + targetId: number; + }[]; + + const insertTargetHealthCheckStmt = db.prepare( + `INSERT INTO targetHealthCheck (targetId) VALUES (?)` + ); + + for (const target of targets) { + insertTargetHealthCheckStmt.run(target.targetId); + } + + db.prepare( + `CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');` + ).run(); + })(); + + console.log(`${version} migration complete`); +} diff --git a/src/actions/server.ts b/src/actions/server.ts new file mode 100644 index 00000000..b9dc6e55 --- /dev/null +++ b/src/actions/server.ts @@ -0,0 +1,417 @@ +"use server"; + +import { cookies, headers as reqHeaders } from "next/headers"; +import { ResponseT } from "@server/types/Response"; +import { pullEnv } from "@app/lib/pullEnv"; + +type CookieOptions = { + path?: string; + httpOnly?: boolean; + secure?: boolean; + sameSite?: "lax" | "strict" | "none"; + expires?: Date; + maxAge?: number; + domain?: string; +}; + +function parseSetCookieString( + setCookie: string, + host?: string +): { + name: string; + value: string; + options: CookieOptions; +} { + const parts = setCookie.split(";").map((p) => p.trim()); + const [nameValue, ...attrParts] = parts; + const [name, ...valParts] = nameValue.split("="); + const value = valParts.join("="); // handles '=' inside JWT + + const env = pullEnv(); + + const options: CookieOptions = {}; + + for (const attr of attrParts) { + const [k, v] = attr.split("=").map((s) => s.trim()); + switch (k.toLowerCase()) { + case "path": + options.path = v; + break; + case "httponly": + options.httpOnly = true; + break; + case "secure": + options.secure = true; + break; + case "samesite": + options.sameSite = + v?.toLowerCase() as CookieOptions["sameSite"]; + break; + case "expires": + options.expires = new Date(v); + break; + case "max-age": + options.maxAge = parseInt(v, 10); + break; + } + } + + if (!options.domain) { + const d = host + ? host.split(":")[0] // strip port if present + : new URL(env.app.dashboardUrl).hostname; + if (d) { + options.domain = d; + } + } + + return { name, value, options }; +} + +async function makeApiRequest( + url: string, + method: "GET" | "POST", + body?: any, + additionalHeaders: Record = {} +): Promise> { + // Get existing cookies to forward + const allCookies = await cookies(); + const cookieHeader = allCookies.toString(); + + const headersList = await reqHeaders(); + const host = headersList.get("host"); + + const headers: Record = { + "Content-Type": "application/json", + "X-CSRF-Token": "x-csrf-protection", + ...(cookieHeader && { Cookie: cookieHeader }), + ...additionalHeaders + }; + + let res: Response; + try { + res = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + cache: "no-store" + }); + } catch (fetchError) { + console.error("API request failed:", fetchError); + return { + data: null, + success: false, + error: true, + message: "Failed to connect to server. Please try again.", + status: 0 + }; + } + + // Handle Set-Cookie header + const rawSetCookie = res.headers.get("set-cookie"); + if (rawSetCookie) { + try { + const { name, value, options } = parseSetCookieString( + rawSetCookie, + host || undefined + ); + const allCookies = await cookies(); + allCookies.set(name, value, options); + } catch (cookieError) { + console.error("Failed to parse Set-Cookie header:", cookieError); + // Continue without setting cookies rather than failing + } + } + + let responseData; + try { + responseData = await res.json(); + } catch (jsonError) { + console.error("Failed to parse response JSON:", jsonError); + return { + data: null, + success: false, + error: true, + message: "Invalid response format from server. Please try again.", + status: res.status + }; + } + + if (!responseData) { + console.error("Invalid response structure:", responseData); + return { + data: null, + success: false, + error: true, + message: + "Invalid response structure from server. Please try again.", + status: res.status + }; + } + + // If the API returned an error, return the error message + if (!res.ok || responseData.error) { + return { + data: null, + success: false, + error: true, + message: + responseData.message || + `Server responded with ${res.status}: ${res.statusText}`, + status: res.status + }; + } + + // Handle successful responses where data can be null + if (responseData.success && responseData.data === null) { + return { + data: null, + success: true, + error: false, + message: responseData.message || "Success", + status: res.status + }; + } + + if (!responseData.data) { + console.error("Invalid response structure:", responseData); + return { + data: null, + success: false, + error: true, + message: + "Invalid response structure from server. Please try again.", + status: res.status + }; + } + + return { + data: responseData.data, + success: true, + error: false, + message: responseData.message || "Success", + status: res.status + }; +} + +// ============================================================================ +// AUTH TYPES AND FUNCTIONS +// ============================================================================ + +export type LoginRequest = { + email: string; + password: string; + code?: string; +}; + +export type LoginResponse = { + useSecurityKey?: boolean; + codeRequested?: boolean; + emailVerificationRequired?: boolean; + twoFactorSetupRequired?: boolean; +}; + +export type SecurityKeyStartRequest = { + email?: string; +}; + +export type SecurityKeyStartResponse = { + tempSessionId: string; + challenge: string; + allowCredentials: any[]; + timeout: number; + rpId: string; + userVerification: "required" | "preferred" | "discouraged"; +}; + +export type SecurityKeyVerifyRequest = { + credential: any; +}; + +export type SecurityKeyVerifyResponse = { + success: boolean; + message?: string; +}; + +export async function loginProxy( + request: LoginRequest +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/login`; + + console.log("Making login request to:", url); + + return await makeApiRequest(url, "POST", request); +} + +export async function securityKeyStartProxy( + request: SecurityKeyStartRequest +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/security-key/authenticate/start`; + + console.log("Making security key start request to:", url); + + return await makeApiRequest(url, "POST", request); +} + +export async function securityKeyVerifyProxy( + request: SecurityKeyVerifyRequest, + tempSessionId: string +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/security-key/authenticate/verify`; + + console.log("Making security key verify request to:", url); + + return await makeApiRequest( + url, + "POST", + request, + { + "X-Temp-Session-Id": tempSessionId + } + ); +} + +// ============================================================================ +// RESOURCE TYPES AND FUNCTIONS +// ============================================================================ + +export type ResourcePasswordRequest = { + password: string; +}; + +export type ResourcePasswordResponse = { + session?: string; +}; + +export type ResourcePincodeRequest = { + pincode: string; +}; + +export type ResourcePincodeResponse = { + session?: string; +}; + +export type ResourceWhitelistRequest = { + email: string; + otp?: string; +}; + +export type ResourceWhitelistResponse = { + otpSent?: boolean; + session?: string; +}; + +export type ResourceAccessResponse = { + success: boolean; + message?: string; +}; + +export async function resourcePasswordProxy( + resourceId: number, + request: ResourcePasswordRequest +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/password`; + + console.log("Making resource password request to:", url); + + return await makeApiRequest(url, "POST", request); +} + +export async function resourcePincodeProxy( + resourceId: number, + request: ResourcePincodeRequest +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/pincode`; + + console.log("Making resource pincode request to:", url); + + return await makeApiRequest(url, "POST", request); +} + +export async function resourceWhitelistProxy( + resourceId: number, + request: ResourceWhitelistRequest +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/whitelist`; + + console.log("Making resource whitelist request to:", url); + + return await makeApiRequest( + url, + "POST", + request + ); +} + +export async function resourceAccessProxy( + resourceId: number +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/resource/${resourceId}`; + + console.log("Making resource access request to:", url); + + return await makeApiRequest(url, "GET"); +} + +// ============================================================================ +// IDP TYPES AND FUNCTIONS +// ============================================================================ + +export type GenerateOidcUrlRequest = { + redirectUrl: string; +}; + +export type GenerateOidcUrlResponse = { + redirectUrl: string; +}; + +export type ValidateOidcUrlCallbackRequest = { + code: string; + state: string; + storedState: string; +}; + +export type ValidateOidcUrlCallbackResponse = { + redirectUrl: string; +}; + +export async function validateOidcUrlCallbackProxy( + idpId: string, + code: string, + expectedState: string, + stateCookie: string, + loginPageId?: number +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/idp/${idpId}/oidc/validate-callback${loginPageId ? "?loginPageId=" + loginPageId : ""}`; + + console.log("Making OIDC callback validation request to:", url); + + return await makeApiRequest(url, "POST", { + code: code, + state: expectedState, + storedState: stateCookie + }); +} + +export async function generateOidcUrlProxy( + idpId: number, + redirect: string, + orgId?: string +): Promise> { + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/idp/${idpId}/oidc/generate-url${orgId ? `?orgId=${orgId}` : ""}`; + + console.log("Making OIDC URL generation request to:", url); + + return await makeApiRequest(url, "POST", { + redirectUrl: redirect || "/" + }); +} diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index 3ab0b92e..585c9b48 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,8 +1,6 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; -import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; -import UserProvider from "@app/providers/UserProvider"; import { GetOrgResponse } from "@server/routers/org"; import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; @@ -51,9 +49,6 @@ export default async function OrgLayout(props: { } return ( - <> - {props.children} - - + ); } diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 4c3ac07b..25b3de1f 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,7 +1,6 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; -import OrganizationLandingCard from "../../components/OrganizationLandingCard"; import MemberResourcesPortal from "../../components/MemberResourcesPortal"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 2df8413f..424a561a 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -45,7 +45,6 @@ import { createApiClient } from "@app/lib/api"; import { Checkbox } from "@app/components/ui/checkbox"; import { ListIdpsResponse } from "@server/routers/idp"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; import Image from "next/image"; type UserType = "internal" | "oidc"; @@ -74,7 +73,9 @@ export default function Page() { const api = createApiClient({ env }); const t = useTranslations(); - const [selectedOption, setSelectedOption] = useState("internal"); + const [selectedOption, setSelectedOption] = useState( + "internal" + ); const [inviteLink, setInviteLink] = useState(null); const [loading, setLoading] = useState(false); const [expiresInDays, setExpiresInDays] = useState(1); @@ -199,7 +200,13 @@ export default function Page() { googleAzureForm.reset(); genericOidcForm.reset(); } - }, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]); + }, [ + selectedOption, + env.email.emailEnabled, + internalForm, + googleAzureForm, + genericOidcForm + ]); useEffect(() => { if (!selectedOption) { @@ -334,7 +341,9 @@ export default function Page() { async function onSubmitGoogleAzure( values: z.infer ) { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + const selectedUserOption = userOptions.find( + (opt) => opt.id === selectedOption + ); if (!selectedUserOption?.idpId) return; setLoading(true); @@ -374,7 +383,9 @@ export default function Page() { async function onSubmitGenericOidc( values: z.infer ) { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); + const selectedUserOption = userOptions.find( + (opt) => opt.id === selectedOption + ); if (!selectedUserOption?.idpId) return; setLoading(true); @@ -430,7 +441,7 @@ export default function Page() {
- {!inviteLink && build !== "saas" && dataLoaded ? ( + {!inviteLink ? ( @@ -664,214 +675,284 @@ export default function Page() { )} - {selectedOption && selectedOption !== "internal" && dataLoaded && ( - - - - {t("userSettings")} - - - {t("userSettingsDescription")} - - - - - {/* Google/Azure Form */} - {(() => { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); - return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure"; - })() && ( -
- + + + {t("userSettings")} + + + {t("userSettingsDescription")} + + + + + {/* Google/Azure Form */} + {(() => { + const selectedUserOption = + userOptions.find( + (opt) => + opt.id === + selectedOption + ); + return ( + selectedUserOption?.variant === + "google" || + selectedUserOption?.variant === + "azure" + ); + })() && ( + + + ( + + + {t("email")} + + + + + + )} - className="space-y-4" - id="create-user-form" - > - ( - - - {t("email")} - - - - - - - )} - /> + /> - ( - - - {t("nameOptional")} - - - - - - - )} - /> + ( + + + {t( + "nameOptional" + )} + + + + + + + )} + /> - ( - - - {t("role")} - - + + + + + + + {roles.map( + ( + role + ) => ( - {role.name} + { + role.name + } - ))} - - - - - )} - /> - - - )} - - {/* Generic OIDC Form */} - {(() => { - const selectedUserOption = userOptions.find(opt => opt.id === selectedOption); - return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure"; - })() && ( -
- + + + )} - className="space-y-4" - id="create-user-form" - > - ( - - - {t("username")} - - - - -

- {t("usernameUniq")} -

- -
- )} - /> + /> + + + )} - ( - - - {t("emailOptional")} - - - - - - - )} - /> + {/* Generic OIDC Form */} + {(() => { + const selectedUserOption = + userOptions.find( + (opt) => + opt.id === + selectedOption + ); + return ( + selectedUserOption?.variant !== + "google" && + selectedUserOption?.variant !== + "azure" + ); + })() && ( +
+ + ( + + + {t( + "username" + )} + + + + +

+ {t( + "usernameUniq" + )} +

+ +
+ )} + /> - ( - - - {t("nameOptional")} - - - - - - - )} - /> + ( + + + {t( + "emailOptional" + )} + + + + + + + )} + /> - ( - - - {t("role")} - - + + + + )} + /> + + ( + + + {t("role")} + + - - - )} - /> - - - )} -
-
-
- )} + ) + )} + + + + + )} + /> + + + )} + + +
+ )}
diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx index 55d7c0d3..c7171c8d 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx @@ -115,7 +115,7 @@ export default function GeneralPage() { try { await api.post(`/client/${client?.clientId}`, { name: data.name, - siteIds: data.siteIds.map(site => site.id) + siteIds: data.siteIds.map(site => parseInt(site.id)) }); updateClient({ name: data.name }); diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 8155a2d6..e5765aea 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -150,40 +150,41 @@ export default function Page() { const commands = { mac: { "Apple Silicon (arm64)": [ - `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_arm64" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], "Intel x64 (amd64)": [ - `curl -L -o olm "https://github.com/fosrl/olm/releases/download/${version}/olm_darwin_amd64" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, linux: { amd64: [ - `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_amd64" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm64: [ - `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm64" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm32: [ - `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], arm32v6: [ - `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_arm32v6" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ], riscv64: [ - `wget -O olm "https://github.com/fosrl/olm/releases/download/${version}/olm_linux_riscv64" && chmod +x ./olm`, - `sudo ./olm --id ${id} --secret ${secret} --endpoint ${endpoint}` + `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, + `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, windows: { x64: [ + `# Download and run the installer`, `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`, - `# Run the installer to install olm and wintun`, + `# Then run olm with your credentials`, `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` ] } diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index 994b1d56..0813ad3c 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -5,6 +5,7 @@ import { ClientRow } from "../../../../components/ClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListClientsResponse } from "@server/routers/client"; import ClientsTable from "../../../../components/ClientsTable"; +import { getTranslations } from "next-intl/server"; type ClientsPageProps = { params: Promise<{ orgId: string }>; @@ -13,6 +14,8 @@ type ClientsPageProps = { export const dynamic = "force-dynamic"; export default async function ClientsPage(props: ClientsPageProps) { + const t = await getTranslations(); + const params = await props.params; let clients: ListClientsResponse["clients"] = []; try { @@ -48,8 +51,8 @@ export default async function ClientsPage(props: ClientsPageProps) { return ( <> diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index c4bb3ccc..19352408 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -1,10 +1,14 @@ "use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import AuthPageSettings, { + AuthPageSettingsRef +} from "@app/components/private/AuthPageSettings"; + import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { toast } from "@app/hooks/useToast"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { Form, FormControl, @@ -15,6 +19,7 @@ import { FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; + import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -36,9 +41,8 @@ import { } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -// Updated schema to include subnet field +// Schema for general organization settings const GeneralFormSchema = z.object({ name: z.string(), subnet: z.string().optional() @@ -58,6 +62,7 @@ export default function GeneralPage() { const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); + const authPageSettingsRef = useRef(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), @@ -121,28 +126,36 @@ export default function GeneralPage() { async function onSubmit(data: GeneralFormValues) { setLoadingSave(true); - await api - .post(`/org/${org?.org.orgId}`, { - name: data.name, + + try { + // Update organization + await api.post(`/org/${org?.org.orgId}`, { + name: data.name // subnet: data.subnet // Include subnet in the API request - }) - .then(() => { - toast({ - title: t("orgUpdated"), - description: t("orgUpdatedDescription") - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("orgErrorUpdate"), - description: formatAxiosError(e, t("orgErrorUpdateMessage")) - }); - }) - .finally(() => { - setLoadingSave(false); }); + + // Also save auth page settings if they have unsaved changes + /* if ( + build === "saas" && + authPageSettingsRef.current?.hasUnsavedChanges() + ) { + await authPageSettingsRef.current.saveAuthSettings(); + } */ + + toast({ + title: t("orgUpdated"), + description: t("orgUpdatedDescription") + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("orgErrorUpdate"), + description: formatAxiosError(e, t("orgErrorUpdateMessage")) + }); + } finally { + setLoadingSave(false); + } } return ( @@ -207,7 +220,9 @@ export default function GeneralPage() { name="subnet" render={({ field }) => ( - Subnet + + {t("subnet")} + - The subnet for this - organization's network - configuration. + {t("subnetDescription")} )} @@ -228,40 +241,45 @@ export default function GeneralPage() { + + + {/*(build === "saas") && ( + + )*/} + + {/* Save Button */} +
+ +
+ + + + + {t("orgDangerZone")} + + + {t("orgDangerZoneDescription")} + + - {build === "oss" && ( - - - - {t("orgDangerZone")} - - - {t("orgDangerZoneDescription")} - - - - - - - )} ); } diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 7db530dd..d35af6e6 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -27,7 +27,7 @@ import { orgNavSections } from "@app/app/navigation"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { - title: `Settings - Pangolin`, + title: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, description: "" }; diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index d53cb0c0..bd38e541 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -13,7 +13,7 @@ import { ListResourceUsersResponse } from "@server/routers/resource"; import { Button } from "@app/components/ui/button"; -import { set, z } from "zod"; +import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { @@ -26,9 +26,10 @@ import { FormMessage } from "@app/components/ui/form"; import { ListUsersResponse } from "@server/routers/user"; -import { Binary, Key } from "lucide-react"; +import { Binary, Key, Bot } from "lucide-react"; import SetResourcePasswordForm from "../../../../../../components/SetResourcePasswordForm"; import SetResourcePincodeForm from "../../../../../../components/SetResourcePincodeForm"; +import SetResourceHeaderAuthForm from "../../../../../../components/SetResourceHeaderAuthForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { @@ -57,7 +58,6 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { Separator } from "@app/components/ui/separator"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -134,9 +134,14 @@ export default function ResourceAuthenticationPage() { useState(false); const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = useState(false); + const [ + loadingRemoveResourceHeaderAuth, + setLoadingRemoveResourceHeaderAuth + ] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); const usersRolesForm = useForm({ resolver: zodResolver(UsersRolesFormSchema), @@ -412,6 +417,37 @@ export default function ResourceAuthenticationPage() { .finally(() => setLoadingRemoveResourcePincode(false)); } + function removeResourceHeaderAuth() { + setLoadingRemoveResourceHeaderAuth(true); + + api.post(`/resource/${resource.resourceId}/header-auth`, { + user: null, + password: null + }) + .then(() => { + toast({ + title: t("resourceHeaderAuthRemove"), + description: t("resourceHeaderAuthRemoveDescription") + }); + + updateAuthInfo({ + headerAuth: false + }); + router.refresh(); + }) + .catch((e) => { + toast({ + variant: "destructive", + title: t("resourceErrorHeaderAuthRemove"), + description: formatAxiosError( + e, + t("resourceErrorHeaderAuthRemoveDescription") + ) + }); + }) + .finally(() => setLoadingRemoveResourceHeaderAuth(false)); + } + if (pageLoading) { return <>; } @@ -446,6 +482,20 @@ export default function ResourceAuthenticationPage() { /> )} + {isSetHeaderAuthOpen && ( + { + setIsSetHeaderAuthOpen(false); + updateAuthInfo({ + headerAuth: true + }); + }} + /> + )} + @@ -761,6 +811,38 @@ export default function ResourceAuthenticationPage() { : t("pincodeAdd")}
+ + {/* Header Authentication Protection */} +
+
+ + + {authInfo.headerAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
diff --git a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx index 21d601ed..66ec23cf 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx @@ -52,7 +52,6 @@ import { } from "@app/components/Credenza"; import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; -import { build } from "@server/build"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { DomainRow } from "../../../../../../components/DomainsTable"; import { toASCII, toUnicode } from "punycode"; diff --git a/src/app/[orgId]/settings/resources/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/[niceId]/layout.tsx index 3f8425ce..8c140333 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/layout.tsx @@ -46,7 +46,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { try { const res = await internal.get< AxiosResponse - >(`/resource/${resource.resourceId}/auth`, await authCookieHeader()); + >(`/resource/${resource.resourceGuid}/auth`, await authCookieHeader()); authInfo = res.data.data; } catch { redirect(`/${params.orgId}/settings/resources`); diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 4c2eedf5..04db3de5 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -73,11 +73,15 @@ import { CircleCheck, CircleX, ArrowRight, - MoveRight + Plus, + MoveRight, + ArrowUp, + Info, + ArrowDown } from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; +import HealthCheckDialog from "@/components/HealthCheckDialog"; import { DockerManager, DockerState } from "@app/lib/docker"; import { Container } from "@server/routers/site"; import { @@ -95,50 +99,92 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { Badge } from "@app/components/ui/badge"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { HeadersInput } from "@app/components/HeadersInput"; +import { + PathMatchDisplay, + PathMatchModal, + PathRewriteDisplay, + PathRewriteModal +} from "@app/components/PathMatchRenameModal"; +import { Badge } from "@app/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@app/components/ui/tooltip"; -const addTargetSchema = z.object({ - ip: z.string().refine(isTargetValid), - method: z.string().nullable(), - port: z.coerce.number().int().positive(), - siteId: z.number().int().positive(), - path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() -}).refine( - (data) => { - // If path is provided, pathMatchType must be provided - if (data.path && !data.pathMatchType) { - return false; - } - // If pathMatchType is provided, path must be provided - if (data.pathMatchType && !data.path) { - return false; - } - // Validate path based on pathMatchType - if (data.path && data.pathMatchType) { - switch (data.pathMatchType) { - case "exact": - case "prefix": - // Path should start with / - return data.path.startsWith("/"); - case "regex": - // Validate regex - try { - new RegExp(data.path); - return true; - } catch { - return false; - } +const addTargetSchema = z + .object({ + ip: z.string().refine(isTargetValid), + method: z.string().nullable(), + port: z.coerce.number().int().positive(), + siteId: z + .number() + .int() + .positive({ message: "You must select a site for a target." }), + 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.number().int().min(1).max(1000).optional() + }) + .refine( + (data) => { + // If path is provided, pathMatchType must be provided + if (data.path && !data.pathMatchType) { + return false; } + // If pathMatchType is provided, path must be provided + if (data.pathMatchType && !data.path) { + return false; + } + // Validate path based on pathMatchType + if (data.path && data.pathMatchType) { + switch (data.pathMatchType) { + case "exact": + case "prefix": + // Path should start with / + return data.path.startsWith("/"); + case "regex": + // Validate regex + try { + new RegExp(data.path); + return true; + } catch { + return false; + } + } + } + return true; + }, + { + message: "Invalid path configuration" } - return true; - }, - { - message: "Invalid path configuration" - } -); + ) + .refine( + (data) => { + // If rewritePath is provided, rewritePathType must be provided + if (data.rewritePath && !data.rewritePathType) { + return false; + } + // If rewritePathType is provided, rewritePath must be provided + if (data.rewritePathType && !data.rewritePath) { + return false; + } + return true; + }, + { + message: "Invalid rewrite path configuration" + } + ); const targetsSettingsSchema = z.object({ stickySession: z.boolean() @@ -158,6 +204,7 @@ export default function ReverseProxyTargets(props: { }) { const params = use(props.params); const t = useTranslations(); + const { env } = useEnvContext(); const { resource, updateResource } = useResourceContext(); @@ -210,6 +257,17 @@ export default function ReverseProxyTargets(props: { const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); + const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("proxy-advanced-mode"); + return saved === "true"; + } + return false; + }); + const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = + useState(null); const router = useRouter(); const proxySettingsSchema = z.object({ @@ -227,7 +285,9 @@ export default function ReverseProxyTargets(props: { message: t("proxyErrorInvalidHeader") } ), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable() + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable() }); const tlsSettingsSchema = z.object({ @@ -252,28 +312,6 @@ export default function ReverseProxyTargets(props: { type TlsSettingsValues = z.infer; type TargetsSettingsValues = z.infer; - const addTargetForm = useForm({ - resolver: zodResolver(addTargetSchema), - defaultValues: { - ip: "", - method: resource.http ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null - } - }); - - const watchedIp = addTargetForm.watch("ip"); - const watchedPort = addTargetForm.watch("port"); - const watchedSiteId = addTargetForm.watch("siteId"); - - const handleContainerSelect = (hostname: string, port?: number) => { - addTargetForm.setValue("ip", hostname); - if (port) { - addTargetForm.setValue("port", port); - } - }; - const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), defaultValues: { @@ -350,13 +388,7 @@ export default function ReverseProxyTargets(props: { initializeDockerForSite(site.siteId); } - // If there's only one site, set it as the default in the form - if (res.data.data.sites.length) { - addTargetForm.setValue( - "siteId", - res.data.data.sites[0].siteId - ); - } + // Sites loaded successfully } }; fetchSites(); @@ -385,6 +417,166 @@ export default function ReverseProxyTargets(props: { // fetchSite(); }, []); + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "proxy-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + function addNewTarget() { + const isHttp = resource.http; + + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: isHttp ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: isHttp ? null : null, + pathMatchType: isHttp ? null : null, + rewritePath: isHttp ? null : null, + rewritePathType: isHttp ? null : null, + priority: isHttp ? 100 : 100, + enabled: true, + resourceId: resource.resourceId, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } + + async function saveNewTarget(target: LocalTarget) { + // Validate the target + if (!isTargetValid(target.ip)) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidIp"), + description: t("targetErrorInvalidIpDescription") + }); + return; + } + + if (!target.port || target.port <= 0) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidPort"), + description: t("targetErrorInvalidPortDescription") + }); + return; + } + + if (!target.siteId) { + toast({ + variant: "destructive", + title: t("targetErrorNoSite"), + description: t("targetErrorNoSiteDescription") + }); + return; + } + + // Check if target with same IP, port and method already exists + const isDuplicate = targets.some( + (t) => + t.targetId !== target.targetId && + t.ip === target.ip && + t.port === target.port && + t.method === target.method && + t.siteId === target.siteId + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("targetErrorDuplicate"), + description: t("targetErrorDuplicateDescription") + }); + return; + } + + try { + setTargetsLoading(true); + + const data: any = { + resourceId: resource.resourceId, + siteId: target.siteId, + ip: target.ip, + method: target.method, + port: target.port, + enabled: target.enabled, + hcEnabled: target.hcEnabled, + hcPath: target.hcPath, + hcInterval: target.hcInterval, + hcTimeout: target.hcTimeout + }; + + // Only include path-related fields for HTTP resources + if (resource.http) { + data.path = target.path; + data.pathMatchType = target.pathMatchType; + data.rewritePath = target.rewritePath; + data.rewritePathType = target.rewritePathType; + data.priority = target.priority; + } + + const response = await api.post< + AxiosResponse + >(`/target`, data); + + if (response.status === 200) { + // Update the target with the new ID and remove the new flag + setTargets((prev) => + prev.map((t) => + t.targetId === target.targetId + ? { + ...t, + targetId: response.data.data.targetId, + new: false, + updated: false + } + : t + ) + ); + + toast({ + title: t("targetCreated"), + description: t("targetCreatedDescription") + }); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("targetErrorCreate"), + description: formatAxiosError( + err, + t("targetErrorCreateDescription") + ) + }); + } finally { + setTargetsLoading(false); + } + } + async function addTarget(data: z.infer) { // Check if target with same IP, port and method already exists const isDuplicate = targets.some( @@ -431,26 +623,37 @@ export default function ReverseProxyTargets(props: { // } const site = sites.find((site) => site.siteId === data.siteId); + const isHttp = resource.http; const newTarget: LocalTarget = { ...data, - path: data.path || null, - pathMatchType: data.pathMatchType || null, + path: isHttp ? (data.path || null) : null, + pathMatchType: isHttp ? (data.pathMatchType || null) : null, + rewritePath: isHttp ? (data.rewritePath || null) : null, + rewritePathType: isHttp ? (data.rewritePathType || null) : null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, - resourceId: resource.resourceId + resourceId: resource.resourceId, + priority: isHttp ? (data.priority || 100) : 100, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null }; setTargets([...targets, newTarget]); - addTargetForm.reset({ - ip: "", - method: resource.http ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null - }); } const removeTarget = (targetId: number) => { @@ -472,14 +675,52 @@ export default function ReverseProxyTargets(props: { ...target, ...data, updated: true, - siteType: site?.type || null + siteType: site ? site.type : target.siteType } : target ) ); } + function updateTargetHealthCheck(targetId: number, config: any) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...config, + updated: true + } + : target + ) + ); + } + + const openHealthCheckDialog = (target: LocalTarget) => { + console.log(target); + setSelectedTargetForHealthCheck(target); + setHealthCheckDialogOpen(true); + }; + async function saveAllSettings() { + // Validate that no targets have blank IPs or invalid ports + const targetsWithInvalidFields = targets.filter( + (target) => + !target.ip || + target.ip.trim() === "" || + !target.port || + target.port <= 0 || + isNaN(target.port) + ); + if (targetsWithInvalidFields.length > 0) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidIp"), + description: t("targetErrorInvalidIpDescription") + }); + return; + } + try { setTargetsLoading(true); setHttpsTlsLoading(true); @@ -487,16 +728,34 @@ export default function ReverseProxyTargets(props: { // Save targets for (const target of targets) { - const data = { + const data: any = { ip: target.ip, port: target.port, method: target.method, enabled: target.enabled, siteId: target.siteId, - path: target.path, - pathMatchType: target.pathMatchType + hcEnabled: target.hcEnabled, + hcPath: target.hcPath || null, + hcScheme: target.hcScheme || null, + hcHostname: target.hcHostname || null, + hcPort: target.hcPort || null, + hcInterval: target.hcInterval || null, + hcTimeout: target.hcTimeout || null, + hcHeaders: target.hcHeaders || null, + hcFollowRedirects: target.hcFollowRedirects || null, + hcMethod: target.hcMethod || null, + hcStatus: target.hcStatus || null }; + // Only include path-related fields for HTTP resources + if (resource.http) { + data.path = target.path; + data.pathMatchType = target.pathMatchType; + data.rewritePath = target.rewritePath; + data.rewritePathType = target.rewritePathType; + data.priority = target.priority; + } + if (target.new) { const res = await api.put< AxiosResponse @@ -566,102 +825,191 @@ export default function ReverseProxyTargets(props: { } } - const columns: ColumnDef[] = [ - { - accessorKey: "path", - header: t("matchPath"), + const getColumns = (): ColumnDef[] => { + const baseColumns: ColumnDef[] = []; + const isHttp = resource.http; + + const priorityColumn: ColumnDef = { + id: "priority", + header: () => ( +
+ {t("priority")} + + + + + + +

{t("priorityDescription")}

+
+
+
+
+ ), cell: ({ row }) => { - const [showPathInput, setShowPathInput] = useState( - !!(row.original.path || row.original.pathMatchType) - ); - - if (!showPathInput) { - return ( - - ); - } - return ( -
- +
{ - const value = e.target.value.trim(); - if (!value) { - setShowPathInput(false); + const value = parseInt(e.target.value, 10); + if (value >= 1 && value <= 1000) { updateTarget(row.original.targetId, { ...row.original, - path: null, - pathMatchType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - path: value + priority: value }); } }} /> - - -
); - } - }, - { - accessorKey: "siteId", - header: t("site"), + }, + size: 120, + minSize: 100, + maxSize: 150 + }; + + const healthCheckColumn: ColumnDef = { + accessorKey: "healthCheck", + header: t("healthCheck"), + cell: ({ row }) => { + const status = row.original.hcHealth || "unknown"; + const isEnabled = row.original.hcEnabled; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "green"; + case "unhealthy": + return "red"; + case "unknown": + default: + return "secondary"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "healthy": + return t("healthCheckHealthy"); + case "unhealthy": + return t("healthCheckUnhealthy"); + case "unknown": + default: + return t("healthCheckUnknown"); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return ; + case "unhealthy": + return ; + case "unknown": + default: + return null; + } + }; + + return ( +
+ {row.original.siteType === "newt" ? ( + + ) : ( + - + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { + accessorKey: "path", + header: t("matchPath"), + cell: ({ row }) => { + const hasPathMatch = !!( + row.original.path || row.original.pathMatchType + ); + + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { + accessorKey: "address", + header: t("address"), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId @@ -673,246 +1021,336 @@ export default function ReverseProxyTargets(props: { ) => { updateTarget(row.original.targetId, { ...row.original, - ip: hostname + ip: hostname, + ...(port && { port: port }) }); - if (port) { - updateTarget(row.original.targetId, { - ...row.original, - port: port - }); - } }; return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - +
+ {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} + + + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + + updateTarget( row.original - .siteId - ? "opacity-100" - : "opacity-0" - )} - /> - {site.name} - - ))} - - - - - - {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - + + {site.name} + + ))} + + + + + + + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = + /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget( + row.original.targetId, + { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + } + ); + } else { + updateTarget( + row.original.targetId, + { + ...row.original, + ip: input + } + ); } - onRefresh={() => - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} + /> +
); - } - }, - ...(resource.http - ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] - : []), - { - accessorKey: "ip", - header: t("targetAddr"), - cell: ({ row }) => ( - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); + }, + size: 400, + minSize: 350, + maxSize: 500 + }; - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: hasProtocol - ? parsed.protocol - : row.original.method, - ip: parsed.host, - port: hasPort - ? parsed.port - : row.original.port - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) - }, - // { - // accessorKey: "protocol", - // header: t('targetProtocol'), - // cell: ({ row }) => ( - // - // ), - // }, - { + const rewritePathColumn: ColumnDef = { + accessorKey: "rewritePath", + header: t("rewritePath"), + cell: ({ row }) => { + const hasRewritePath = !!( + row.original.rewritePath || row.original.rewritePathType + ); + const noPathMatch = + !row.original.path && !row.original.pathMatchType; + + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const enabledColumn: ColumnDef = { accessorKey: "enabled", header: t("enabled"), cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- {/* */} +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; - -
- - ) + if (isAdvancedMode) { + const columns = [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + + // Only include path-related columns for HTTP resources + if (isHttp) { + columns.unshift(matchPathColumn); + columns.splice(3, 0, rewritePathColumn, priorityColumn); + } + + return columns; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -943,352 +1381,9 @@ export default function ReverseProxyTargets(props: { -
-
- -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - (site) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" - ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() - : null; - })()} -
- -
- )} - /> - - {resource.http && ( - ( - - - {t("method")} - - - - - - - )} - /> - )} - - ( - - - {t("targetAddr")} - - - { - const input = - e.target.value.trim(); - const hasProtocol = - /^(https?|h2c):\/\//.test( - input - ); - const hasPort = - /:\d+(?:\/|$)/.test( - input - ); - - if ( - hasProtocol || - hasPort - ) { - const parsed = - parseHostTarget( - input - ); - if (parsed) { - if ( - hasProtocol || - !addTargetForm.getValues( - "method" - ) - ) { - addTargetForm.setValue( - "method", - parsed.protocol - ); - } - addTargetForm.setValue( - "ip", - parsed.host - ); - if ( - hasPort || - !addTargetForm.getValues( - "port" - ) - ) { - addTargetForm.setValue( - "port", - parsed.port - ); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t("targetPort")} - - - - - - - )} - /> - -
-
- -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
- -
- - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - - -
-
+
{table @@ -1356,12 +1451,40 @@ export default function ReverseProxyTargets(props: { {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)} @@ -1387,7 +1510,7 @@ export default function ReverseProxyTargets(props: { className="space-y-4" id="tls-settings-form" > - {build == "oss" && ( + {!env.flags.usePangolinDns && ( + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + +
+
( - + {t("customHeaders")} { field.onChange( value @@ -1489,6 +1653,11 @@ export default function ReverseProxyTargets(props: { rows={4} /> + + {t( + "customHeadersDescription" + )} + )} @@ -1517,12 +1686,61 @@ export default function ReverseProxyTargets(props: { {t("saveSettings")}
+ + {selectedTargetForHealthCheck && ( + { + if (selectedTargetForHealthCheck) { + console.log(config); + updateTargetHealthCheck( + selectedTargetForHealthCheck.targetId, + config + ); + } + }} + /> + )} ); } function isIPInSubnet(subnet: string, ip: string): boolean { - // Split subnet into IP and mask parts const [subnetIP, maskBits] = subnet.split("/"); const mask = parseInt(maskBits); diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 284573b2..b8459293 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -58,7 +58,7 @@ import { import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules"; import { SwitchInput } from "@app/components/SwitchInput"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { ArrowUpDown, Check, InfoIcon, X } from "lucide-react"; +import { ArrowUpDown, Check, InfoIcon, X, ChevronsUpDown } from "lucide-react"; import { InfoSection, InfoSections, @@ -73,6 +73,20 @@ import { import { Switch } from "@app/components/ui/switch"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; +import { COUNTRIES } from "@server/db/countries"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; // Schema for rule validation const addRuleSchema = z.object({ @@ -98,9 +112,13 @@ export default function ResourceRules(props: { const [loading, setLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules); + const [openCountrySelect, setOpenCountrySelect] = useState(false); + const [countrySelectValue, setCountrySelectValue] = useState(""); + 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 RuleAction = { ACCEPT: t('alwaysAllow'), @@ -111,7 +129,8 @@ export default function ResourceRules(props: { const RuleMatch = { PATH: t('path'), IP: "IP", - CIDR: t('ipAddressRange') + CIDR: t('ipAddressRange'), + GEOIP: t('country') } as const; const addRuleForm = useForm({ @@ -193,6 +212,15 @@ export default function ResourceRules(props: { setLoading(false); return; } + if (data.match === "GEOIP" && !COUNTRIES.some(c => c.code === data.value)) { + toast({ + variant: "destructive", + title: t('rulesErrorInvalidCountry'), + description: t('rulesErrorInvalidCountryDescription') || "Invalid country code." + }); + setLoading(false); + return; + } // find the highest priority and add one let priority = data.priority; @@ -242,6 +270,8 @@ export default function ResourceRules(props: { return t('rulesMatchIpAddress'); case "PATH": return t('rulesMatchUrl'); + case "GEOIP": + return t('rulesMatchCountry'); } } @@ -461,8 +491,8 @@ export default function ResourceRules(props: { cell: ({ row }) => ( ) @@ -480,15 +513,61 @@ export default function ResourceRules(props: { accessorKey: "value", header: t('value'), cell: ({ row }) => ( - - updateRule(row.original.ruleId, { - value: e.target.value - }) - } - /> + row.original.match === "GEOIP" ? ( + + + + + + + + + {t('noCountryFound')} + + {COUNTRIES.map((country) => ( + { + updateRule(row.original.ruleId, { value: country.code }); + }} + > + + {country.name} ({country.code}) + + ))} + + + + + + ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) ) }, { @@ -650,9 +729,7 @@ export default function ResourceRules(props: { @@ -692,7 +774,55 @@ export default function ResourceRules(props: { } /> - + {addRuleForm.watch("match") === "GEOIP" ? ( + + + + + + + + + {t('noCountryFound')} + + {COUNTRIES.map((country) => ( + { + field.onChange(country.code); + setOpenAddRuleCountrySelect(false); + }} + > + + {country.name} ({country.code}) + + ))} + + + + + + ) : ( + + )} diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index f551e418..e4755f3b 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -25,7 +25,6 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; import { Button } from "@app/components/ui/button"; -import { Checkbox } from "@app/components/ui/checkbox"; import { useParams, useRouter } from "next/navigation"; import { ListSitesResponse } from "@server/routers/site"; import { formatAxiosError } from "@app/lib/api"; @@ -58,12 +57,20 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { ArrowRight, MoveRight, SquareArrowOutUpRight } from "lucide-react"; +import { + ArrowRight, + CircleCheck, + CircleX, + Info, + MoveRight, + Plus, + Settings, + SquareArrowOutUpRight +} from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; import DomainPicker from "@app/components/DomainPicker"; -import { build } from "@server/build"; import { ContainersSelector } from "@app/components/ContainersSelector"; import { ColumnDef, @@ -89,10 +96,24 @@ import { isTargetValid } from "@server/lib/validators"; import { ListTargetsResponse } from "@server/routers/target"; import { DockerManager, DockerState } from "@app/lib/docker"; import { parseHostTarget } from "@app/lib/parseHostTarget"; -import { toASCII, toUnicode } from 'punycode'; +import { toASCII, toUnicode } from "punycode"; import { DomainRow } from "../../../../../components/DomainsTable"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; - +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@app/components/ui/tooltip"; +import { + PathMatchDisplay, + PathMatchModal, + PathRewriteDisplay, + PathRewriteModal +} from "@app/components/PathMatchRenameModal"; +import { Badge } from "@app/components/ui/badge"; +import HealthCheckDialog from "@app/components/HealthCheckDialog"; +import { SwitchInput } from "@app/components/SwitchInput"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -110,46 +131,73 @@ const tcpUdpResourceFormSchema = z.object({ // enableProxy: z.boolean().default(false) }); -const addTargetSchema = z.object({ - ip: z.string().refine(isTargetValid), - method: z.string().nullable(), - port: z.coerce.number().int().positive(), - siteId: z.number().int().positive(), - path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() -}).refine( - (data) => { - // If path is provided, pathMatchType must be provided - if (data.path && !data.pathMatchType) { - return false; - } - // If pathMatchType is provided, path must be provided - if (data.pathMatchType && !data.path) { - return false; - } - // Validate path based on pathMatchType - if (data.path && data.pathMatchType) { - switch (data.pathMatchType) { - case "exact": - case "prefix": - // Path should start with / - return data.path.startsWith("/"); - case "regex": - // Validate regex - try { - new RegExp(data.path); - return true; - } catch { - return false; - } +const addTargetSchema = z + .object({ + ip: z.string().refine(isTargetValid), + method: z.string().nullable(), + port: z.coerce.number().int().positive(), + siteId: z.number().int().positive(), + 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.number().int().min(1).max(1000).optional() + }) + .refine( + (data) => { + // If path is provided, pathMatchType must be provided + if (data.path && !data.pathMatchType) { + return false; } + // If pathMatchType is provided, path must be provided + if (data.pathMatchType && !data.path) { + return false; + } + // Validate path based on pathMatchType + if (data.path && data.pathMatchType) { + switch (data.pathMatchType) { + case "exact": + case "prefix": + // Path should start with / + return data.path.startsWith("/"); + case "regex": + // Validate regex + try { + new RegExp(data.path); + return true; + } catch { + return false; + } + } + } + return true; + }, + { + message: "Invalid path configuration" } - return true; - }, - { - message: "Invalid path configuration" - } -); + ) + .refine( + (data) => { + // If rewritePath is provided, rewritePathType must be provided + if (data.rewritePath && !data.rewritePathType) { + return false; + } + // If rewritePathType is provided, rewritePath must be provided + if (data.rewritePathType && !data.rewritePath) { + return false; + } + return true; + }, + { + message: "Invalid rewrite path configuration" + } + ); type BaseResourceFormValues = z.infer; type HttpResourceFormValues = z.infer; @@ -187,12 +235,74 @@ export default function Page() { >([]); const [createLoading, setCreateLoading] = useState(false); const [showSnippets, setShowSnippets] = useState(false); - const [resourceId, setResourceId] = useState(null); + const [niceId, setNiceId] = useState(""); // Target management state const [targets, setTargets] = useState([]); const [targetsToRemove, setTargetsToRemove] = useState([]); - const [dockerStates, setDockerStates] = useState>(new Map()); + const [dockerStates, setDockerStates] = useState>( + new Map() + ); + + const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = + useState(null); + const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("create-advanced-mode"); + return saved === "true"; + } + return false; + }); + + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "create-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + function addNewTarget() { + const isHttp = baseForm.watch("http"); + + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: isHttp ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: isHttp ? null : null, + pathMatchType: isHttp ? null : null, + rewritePath: isHttp ? null : null, + rewritePathType: isHttp ? null : null, + priority: isHttp ? 100 : 100, + enabled: true, + resourceId: 0, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } const resourceTypes: ReadonlyArray = [ { @@ -203,12 +313,12 @@ export default function Page() { ...(!env.flags.allowRawResources ? [] : [ - { - id: "raw" as ResourceType, - title: t("resourceRaw"), - description: t("resourceRawDescription") - } - ]) + { + id: "raw" as ResourceType, + title: t("resourceRaw"), + description: t("resourceRawDescription") + } + ]) ]; const baseForm = useForm({ @@ -240,19 +350,42 @@ export default function Page() { method: baseForm.watch("http") ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null - } + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + priority: baseForm.watch("http") ? 100 : undefined + } as z.infer }); - const watchedIp = addTargetForm.watch("ip"); - const watchedPort = addTargetForm.watch("port"); - const watchedSiteId = addTargetForm.watch("siteId"); + // Helper function to check if all targets have required fields using schema validation + const areAllTargetsValid = () => { + if (targets.length === 0) return true; // No targets is valid - const handleContainerSelect = (hostname: string, port?: number) => { - addTargetForm.setValue("ip", hostname); - if (port) { - addTargetForm.setValue("port", port); - } + return targets.every((target) => { + try { + const isHttp = baseForm.watch("http"); + const targetData: any = { + ip: target.ip, + method: target.method, + port: target.port, + siteId: target.siteId, + path: target.path, + pathMatchType: target.pathMatchType, + rewritePath: target.rewritePath, + rewritePathType: target.rewritePathType + }; + + // Only include priority for HTTP resources + if (isHttp) { + targetData.priority = target.priority; + } + + addTargetSchema.parse(targetData); + return true; + } catch { + return false; + } + }); }; const initializeDockerForSite = async (siteId: number) => { @@ -263,14 +396,14 @@ export default function Page() { const dockerManager = new DockerManager(api, siteId); const dockerState = await dockerManager.initializeDocker(); - setDockerStates(prev => new Map(prev.set(siteId, dockerState))); + setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; const refreshContainersForSite = async (siteId: number) => { const dockerManager = new DockerManager(api, siteId); const containers = await dockerManager.fetchContainers(); - setDockerStates(prev => { + setDockerStates((prev) => { const newMap = new Map(prev); const existingState = newMap.get(siteId); if (existingState) { @@ -281,11 +414,13 @@ export default function Page() { }; const getDockerStateForSite = (siteId: number): DockerState => { - return dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - }; + return ( + dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + } + ); }; async function addTarget(data: z.infer) { @@ -309,15 +444,34 @@ export default function Page() { const site = sites.find((site) => site.siteId === data.siteId); + const isHttp = baseForm.watch("http"); + const newTarget: LocalTarget = { ...data, - path: data.path || null, - pathMatchType: data.pathMatchType || null, + path: isHttp ? (data.path || null) : null, + pathMatchType: isHttp ? (data.pathMatchType || null) : null, + rewritePath: isHttp ? (data.rewritePath || null) : null, + rewritePathType: isHttp ? (data.rewritePathType || null) : null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, - resourceId: 0 // Will be set when resource is created + resourceId: 0, // Will be set when resource is created + priority: isHttp ? (data.priority || 100) : 100, // Default priority + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null }; setTargets([...targets, newTarget]); @@ -326,7 +480,10 @@ export default function Page() { method: baseForm.watch("http") ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + priority: isHttp ? 100 : undefined }); } @@ -346,11 +503,11 @@ export default function Page() { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site?.type || null - } + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } : target ) ); @@ -365,7 +522,7 @@ export default function Page() { try { const payload = { name: baseData.name, - http: baseData.http + http: baseData.http, }; let sanitizedSubdomain: string | undefined; @@ -378,7 +535,9 @@ export default function Page() { : undefined; Object.assign(payload, { - subdomain: sanitizedSubdomain ? toASCII(sanitizedSubdomain) : undefined, + subdomain: sanitizedSubdomain + ? toASCII(sanitizedSubdomain) + : undefined, domainId: httpData.domainId, protocol: "tcp" }); @@ -409,22 +568,41 @@ export default function Page() { if (res && res.status === 201) { const id = res.data.data.resourceId; const niceId = res.data.data.niceId; - setResourceId(id); + setNiceId(niceId); // Create targets if any exist if (targets.length > 0) { try { for (const target of targets) { - const data = { + const data: any = { ip: target.ip, port: target.port, method: target.method, enabled: target.enabled, siteId: target.siteId, - path: target.path, - pathMatchType: target.pathMatchType + hcEnabled: target.hcEnabled, + hcPath: target.hcPath || null, + hcMethod: target.hcMethod || null, + hcInterval: target.hcInterval || null, + hcTimeout: target.hcTimeout || null, + hcHeaders: target.hcHeaders || null, + hcScheme: target.hcScheme || null, + hcHostname: target.hcHostname || null, + hcPort: target.hcPort || null, + hcFollowRedirects: + target.hcFollowRedirects || null, + hcStatus: target.hcStatus || null }; + // Only include path-related fields for HTTP resources + if (isHttp) { + data.path = target.path; + data.pathMatchType = target.pathMatchType; + data.rewritePath = target.rewritePath; + data.rewritePathType = target.rewritePathType; + data.priority = target.priority; + } + await api.put(`/resource/${id}/target`, data); } } catch (targetError) { @@ -526,7 +704,7 @@ export default function Page() { const rawDomains = res.data.data.domains as DomainRow[]; const domains = rawDomains.map((domain) => ({ ...domain, - baseDomain: toUnicode(domain.baseDomain), + baseDomain: toUnicode(domain.baseDomain) })); setBaseDomains(domains); // if (domains.length) { @@ -544,102 +722,211 @@ export default function Page() { load(); }, []); - const columns: ColumnDef[] = [ - { - accessorKey: "path", - header: t("matchPath"), + function TargetHealthCheck(targetId: number, config: any) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...config, + updated: true + } + : target + ) + ); + } + + const openHealthCheckDialog = (target: LocalTarget) => { + console.log(target); + setSelectedTargetForHealthCheck(target); + setHealthCheckDialogOpen(true); + }; + + const getColumns = (): ColumnDef[] => { + const baseColumns: ColumnDef[] = []; + const isHttp = baseForm.watch("http"); + + const priorityColumn: ColumnDef = { + id: "priority", + header: () => ( +
+ {t("priority")} + + + + + + +

{t("priorityDescription")}

+
+
+
+
+ ), cell: ({ row }) => { - const [showPathInput, setShowPathInput] = useState( - !!(row.original.path || row.original.pathMatchType) - ); - - if (!showPathInput) { - return ( - - ); - } - return ( -
- +
{ - const value = e.target.value.trim(); - if (!value) { - setShowPathInput(false); + const value = parseInt(e.target.value, 10); + if (value >= 1 && value <= 1000) { updateTarget(row.original.targetId, { ...row.original, - path: null, - pathMatchType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - path: value + priority: value }); } }} /> - - -
); - } - }, - { - accessorKey: "siteId", - header: t("site"), + }, + size: 120, + minSize: 100, + maxSize: 150 + }; + + const healthCheckColumn: ColumnDef = { + accessorKey: "healthCheck", + header: t("healthCheck"), + cell: ({ row }) => { + const status = row.original.hcHealth || "unknown"; + const isEnabled = row.original.hcEnabled; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "green"; + case "unhealthy": + return "red"; + case "unknown": + default: + return "secondary"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "healthy": + return t("healthCheckHealthy"); + case "unhealthy": + return t("healthCheckUnhealthy"); + case "unknown": + default: + return t("healthCheckUnknown"); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return ; + case "unhealthy": + return ; + case "unknown": + default: + return null; + } + }; + + return ( +
+ {row.original.siteType === "newt" ? ( + + ) : ( + - + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { + accessorKey: "path", + header: t("matchPath"), + cell: ({ row }) => { + const hasPathMatch = !!( + row.original.path || row.original.pathMatchType + ); + + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { + accessorKey: "address", + header: t("address"), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId @@ -651,206 +938,336 @@ export default function Page() { ) => { updateTarget(row.original.targetId, { ...row.original, - ip: hostname + ip: hostname, + ...(port && { port: port }) }); - if (port) { - updateTarget(row.original.targetId, { - ...row.original, - port: port - }); - } }; return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - +
+ {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} + + + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + + updateTarget( row.original - .siteId - ? "opacity-100" - : "opacity-0" - )} - /> - {site.name} - - ))} - - - - - - {selectedSite && selectedSite.type === "newt" && (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })()} + .targetId, + { + siteId: site.siteId + } + ) + } + > + + {site.name} + + ))} + + + + + + + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = + /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget( + row.original.targetId, + { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + } + ); + } else { + updateTarget( + row.original.targetId, + { + ...row.original, + ip: input + } + ); + } + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} + /> +
); - } - }, - ...(baseForm.watch("http") - ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] - : []), - { - accessorKey: "ip", - header: t("targetAddr"), - cell: ({ row }) => ( - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); + }, + size: 400, + minSize: 350, + maxSize: 500 + }; - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: hasProtocol ? parsed.protocol : row.original.method, - ip: parsed.host, - port: hasPort ? parsed.port : row.original.port - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); - } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) - }, - { + const rewritePathColumn: ColumnDef = { + accessorKey: "rewritePath", + header: t("rewritePath"), + cell: ({ row }) => { + const hasRewritePath = !!( + row.original.rewritePath || row.original.rewritePathType + ); + const noPathMatch = + !row.original.path && !row.original.pathMatchType; + + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )} +
+ ); + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const enabledColumn: ColumnDef = { accessorKey: "enabled", header: t("enabled"), cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- -
- - ) +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + if (isAdvancedMode) { + const columns = [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + + // Only include path-related columns for HTTP resources + if (isHttp) { + columns.unshift(matchPathColumn); + columns.splice(3, 0, rewritePathColumn, priorityColumn); + } + + return columns; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -1091,10 +1508,10 @@ export default function Page() { .target .value ? parseInt( - e - .target - .value - ) + e + .target + .value + ) : undefined ) } @@ -1166,281 +1583,9 @@ export default function Page() { -
- - -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - ( - site - ) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" ? (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })() : null; - })()} -
- -
- )} - /> - - {baseForm.watch("http") && ( - ( - - - {t( - "method" - )} - - - - - - - )} - /> - )} - - ( - - {t("targetAddr")} - - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); - - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - if (hasProtocol || !addTargetForm.getValues("method")) { - addTargetForm.setValue("method", parsed.protocol); - } - addTargetForm.setValue("ip", parsed.host); - if (hasPort || !addTargetForm.getValues("port")) { - addTargetForm.setValue("port", parsed.port); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t( - "targetPort" - )} - - - - - - - )} - /> - -
- - -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
-
+
{table @@ -1466,12 +1611,12 @@ export default function Page() { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} @@ -1530,14 +1675,52 @@ export default function Page() { )} + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)} @@ -1572,10 +1755,75 @@ export default function Page() { } }} loading={createLoading} + disabled={!areAllTargetsValid()} > {t("resourceCreate")}
+ {selectedTargetForHealthCheck && ( + { + if (selectedTargetForHealthCheck) { + console.log(config); + TargetHealthCheck( + selectedTargetForHealthCheck.targetId, + config + ); + } + }} + /> + )} ) : ( @@ -1595,7 +1843,9 @@ export default function Page() { {t("resourceAddEntrypoints")}

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

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

router.push( - `/${orgId}/settings/resources/${resourceId}/proxy` + `/${orgId}/settings/resources/${niceId}/proxy` ) } > diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index 97abdd4c..f4ba9d16 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -86,7 +86,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) { : resource.sso || resource.pincodeId !== null || resource.passwordId !== null || - resource.whitelist + resource.whitelist || + resource.headerAuthId ? "protected" : "not_protected", enabled: resource.enabled, diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index ad5438f7..60fe0c7d 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -42,10 +42,7 @@ import { FaFreebsd, FaWindows } from "react-icons/fa"; -import { - SiNixos, - SiKubernetes -} from "react-icons/si"; +import { SiNixos, SiKubernetes } from "react-icons/si"; import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; @@ -56,13 +53,13 @@ import { CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; +import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; import { QRCodeCanvas } from "qrcode.react"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; type SiteType = "newt" | "wireguard" | "local"; @@ -73,6 +70,13 @@ interface TunnelTypeOption { disabled?: boolean; } +interface RemoteExitNodeOption { + id: string; + title: string; + description: string; + disabled?: boolean; +} + type Commands = { mac: Record; linux: Record; @@ -115,21 +119,9 @@ export default function Page() { method: z.enum(["newt", "wireguard", "local"]), copied: z.boolean(), clientAddress: z.string().optional(), - acceptClients: z.boolean() - }) - .refine( - (data) => { - if (data.method !== "local") { - // return data.copied; - return true; - } - return true; - }, - { - message: t("sitesConfirmCopy"), - path: ["copied"] - } - ); + acceptClients: z.boolean(), + exitNodeId: z.number().optional() + }); type CreateSiteFormValues = z.infer; @@ -148,7 +140,7 @@ export default function Page() { { id: "wireguard" as SiteType, title: t("siteWg"), - description: build == "saas" ? t("siteWgDescriptionSaas") : t("siteWgDescription"), + description: t("siteWgDescription"), disabled: true } ]), @@ -158,7 +150,7 @@ export default function Page() { { id: "local" as SiteType, title: t("local"), - description: build == "saas" ? t("siteLocalDescriptionSaas") : t("siteLocalDescription") + description: t("siteLocalDescription") } ]) ]); @@ -184,6 +176,13 @@ export default function Page() { const [siteDefaults, setSiteDefaults] = useState(null); + const [remoteExitNodeOptions, setRemoteExitNodeOptions] = useState< + ReadonlyArray + >([]); + const [selectedExitNodeId, setSelectedExitNodeId] = useState< + string | undefined + >(); + const hydrateWireGuardConfig = ( privateKey: string, publicKey: string, @@ -320,7 +319,7 @@ WantedBy=default.target` nixos: { All: [ `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], + ] // aarch64: [ // `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ] @@ -432,7 +431,8 @@ WantedBy=default.target` copied: false, method: "newt", clientAddress: "", - acceptClients: false + acceptClients: false, + exitNodeId: undefined } }); @@ -513,14 +513,14 @@ WantedBy=default.target` try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); - + const response = await fetch( `https://api.github.com/repos/fosrl/newt/releases/latest`, { signal: controller.signal } ); - + clearTimeout(timeoutId); - + if (!response.ok) { throw new Error( t("newtErrorFetchReleases", { @@ -533,7 +533,7 @@ WantedBy=default.target` currentNewtVersion = latestVersion; setNewtVersion(latestVersion); } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { + if (error instanceof Error && error.name === "AbortError") { console.error(t("newtErrorFetchTimeout")); } else { console.error( @@ -558,8 +558,10 @@ WantedBy=default.target` await api .get(`/org/${orgId}/pick-site-defaults`) .catch((e) => { - // update the default value of the form to be local method - form.setValue("method", "local"); + // update the default value of the form to be local method only if local sites are not disabled + if (!env.flags.disableLocalSites) { + form.setValue("method", "local"); + } }) .then((res) => { if (res && res.status === 200) { @@ -613,6 +615,18 @@ WantedBy=default.target` form.setValue("acceptClients", acceptClients); }, [acceptClients, form]); + // Sync form exitNodeId value with local state + /* useEffect(() => { + if (build !== "saas") { + // dont update the form + return; + } + form.setValue( + "exitNodeId", + selectedExitNodeId ? parseInt(selectedExitNodeId) : undefined + ); + }, [selectedExitNodeId, form]);*/ + return ( <>
@@ -920,7 +934,7 @@ WantedBy=default.target`
{t('idpErrorNotFound')}
; } + const allHeaders = await headers(); + const host = allHeaders.get("host"); + const env = pullEnv(); + const expectedHost = env.app.dashboardUrl.split("//")[1]; + let loginPage: LoadLoginPageResponse | undefined; + if (host !== expectedHost) { + try { + const res = await priv.get>( + `/login-page?idpId=${foundIdp.idpId}&fullDomain=${host}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) { + console.error(formatAxiosError(e)); + } + + if (!loginPage) { + redirect(env.app.dashboardUrl); + } + } + return ( <>
@@ -29,43 +20,6 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
{children}
- -
); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 1068c4f7..37d741db 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -37,13 +37,14 @@ export default async function Page(props: { redirectUrl = cleanRedirect(searchParams.redirect as string); } + let loginIdps: LoginFormIDP[] = []; const idpsRes = await cache( async () => await priv.get>("/idp") )(); - const loginIdps = idpsRes.data.data.idps.map((idp) => ({ + loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, name: idp.name, - variant: idp.variant + variant: idp.type })) as LoginFormIDP[]; const t = await getTranslations(); diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index 490f89f7..1245ca09 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -5,6 +5,8 @@ import ResetPasswordForm from "@app/components/ResetPasswordForm"; import Link from "next/link"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { getTranslations } from "next-intl/server"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; export const dynamic = "force-dynamic"; @@ -22,7 +24,19 @@ export default async function Page(props: { const t = await getTranslations(); if (user) { - redirect("/"); + let loggedOut = false; + try { + // log out the user if they are logged in + await internal.post( + "/auth/logout", + undefined, + await authCookieHeader() + ); + loggedOut = true; + } catch (e) {} + if (!loggedOut) { + redirect("/"); + } } let redirectUrl: string | undefined = undefined; @@ -45,8 +59,8 @@ export default async function Page(props: { diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx similarity index 80% rename from src/app/auth/resource/[resourceId]/page.tsx rename to src/app/auth/resource/[resourceGuid]/page.tsx index 25580ee7..f905fde3 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -16,11 +16,13 @@ import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; import AutoLoginHandler from "@app/components/AutoLoginHandler"; +import { headers } from "next/headers"; +import { GetLoginPageResponse } from "@server/routers/loginPage/types"; export const dynamic = "force-dynamic"; export default async function ResourceAuthPage(props: { - params: Promise<{ resourceId: number }>; + params: Promise<{ resourceGuid: number }>; searchParams: Promise<{ redirect: string | undefined; token: string | undefined; @@ -37,7 +39,7 @@ export default async function ResourceAuthPage(props: { try { const res = await internal.get< AxiosResponse - >(`/resource/${params.resourceId}/auth`, authHeader); + >(`/resource/${params.resourceGuid}/auth`, authHeader); if (res && res.status === 200) { authInfo = res.data.data; @@ -48,15 +50,34 @@ export default async function ResourceAuthPage(props: { const user = await getUser({ skipCheckVerifyEmail: true }); if (!authInfo) { - // TODO: fix this return (
- {/* @ts-ignore */}
); } + const allHeaders = await headers(); + const host = allHeaders.get("host"); + + const expectedHost = env.app.dashboardUrl.split("//")[1]; + if (host !== expectedHost) { + let loginPage: GetLoginPageResponse | undefined; + try { + const res = await priv.get>( + `/login-page?resourceId=${authInfo.resourceId}&fullDomain=${host}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + if (!loginPage) { + redirect(env.app.dashboardUrl); + } + } + let redirectUrl = authInfo.url; if (searchParams.redirect) { try { @@ -86,7 +107,7 @@ export default async function ResourceAuthPage(props: { if (user && !user.emailVerified && env.flags.emailVerificationRequired) { redirect( - `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}` + `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceGuid}` ); } @@ -103,7 +124,7 @@ export default async function ResourceAuthPage(props: { const res = await priv.post< AxiosResponse >( - `/resource/${params.resourceId}/get-exchange-token`, + `/resource/${authInfo.resourceId}/get-exchange-token`, {}, await authCookieHeader() ); @@ -132,21 +153,28 @@ export default async function ResourceAuthPage(props: {
); } + let loginIdps: LoginFormIDP[] = []; const idpsRes = await cache( async () => await priv.get>("/idp") )(); - const loginIdps = idpsRes.data.data.idps.map((idp) => ({ + loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, - name: idp.name + name: idp.name, + variant: idp.type })) as LoginFormIDP[]; - if (authInfo.skipToIdpId && authInfo.skipToIdpId !== null) { + if ( + !userIsUnauthorized && + isSSOOnly && + authInfo.skipToIdpId && + authInfo.skipToIdpId !== null + ) { const idp = loginIdps.find((idp) => idp.idpId === authInfo.skipToIdpId); if (idp) { return ( @@ -154,6 +182,7 @@ export default async function ResourceAuthPage(props: { resourceId={authInfo.resourceId} skipToIdpId={authInfo.skipToIdpId} redirectUrl={redirectUrl} + orgId={undefined} /> ); } @@ -180,6 +209,7 @@ export default async function ResourceAuthPage(props: { }} redirect={redirectUrl} idps={loginIdps} + orgId={undefined} />
)} diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 49c5a2c5..2e027f77 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -1,11 +1,6 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import { verifySession } from "@app/lib/auth/verifySession"; -import { AcceptInviteResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import InviteStatusCard from "../../components/InviteStatusCard"; -import { formatAxiosError } from "@app/lib/api"; import { getTranslations } from "next-intl/server"; export default async function InvitePage(props: { @@ -27,8 +22,8 @@ export default async function InvitePage(props: { if (parts.length !== 2) { return ( <> -

{t('inviteInvalid')}

-

{t('inviteInvalidDescription')}

+

{t("inviteInvalid")}

+

{t("inviteInvalidDescription")}

); } @@ -36,58 +31,15 @@ export default async function InvitePage(props: { const inviteId = parts[0]; const token = parts[1]; - let error = ""; - const res = await internal - .post>( - `/invite/accept`, - { - inviteId, - token, - }, - await authCookieHeader() - ) - .catch((e) => { - error = formatAxiosError(e); - console.error(error); - }); - - if (res && res.status === 200) { - redirect(`/${res.data.data.orgId}`); - } - - function cardType() { - if (error.includes("Invite is not for this user")) { - return "wrong_user"; - } else if ( - error.includes("User does not exist. Please create an account first.") - ) { - return "user_does_not_exist"; - } else if (error.includes("You must be logged in to accept an invite")) { - return "not_logged_in"; - } else { - return "rejected"; - } - } - - const type = cardType(); - - if (!user && type === "user_does_not_exist") { - const redirectUrl = emailParam - ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` - : `/auth/signup?redirect=/invite?token=${params.token}`; - redirect(redirectUrl); - } - - if (!user && type === "not_logged_in") { - const redirectUrl = emailParam - ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` - : `/auth/login?redirect=/invite?token=${params.token}`; - redirect(redirectUrl); - } - return ( <> - + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a60ffb6c..b0fb8d24 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,20 +2,33 @@ import type { Metadata } from "next"; import "./globals.css"; import { Inter } from "next/font/google"; import { ThemeProvider } from "@app/providers/ThemeProvider"; +import ThemeDataProvider from "@app/providers/ThemeDataProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { pullEnv } from "@app/lib/pullEnv"; import { NextIntlClientProvider } from "next-intl"; import { getLocale } from "next-intl/server"; import { Toaster } from "@app/components/ui/toaster"; +import SplashImage from "@app/components/private/SplashImage"; export const metadata: Metadata = { - title: `Dashboard - Pangolin`, + title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, description: "", + + ...(process.env.BRANDING_FAVICON_PATH + ? { + icons: { + icon: [ + { + url: process.env.BRANDING_FAVICON_PATH as string + } + ] + } + } + : {}) }; export const dynamic = "force-dynamic"; -// const font = Figtree({ subsets: ["latin"] }); const font = Inter({ subsets: ["latin"] }); export default async function RootLayout({ @@ -35,18 +48,34 @@ export default async function RootLayout({ enableSystem disableTransitionOnChange > - - {/* Main content */} -
-
- {children} + + + {/* Main content */} +
+
+ + {children} + +
-
- - + + + ); } + +function loadBrandingColors() { + // this is loaded once on the server and not included in pullEnv + // so we don't need to parse the json every time pullEnv is called + if (process.env.BRANDING_COLORS) { + try { + return JSON.parse(process.env.BRANDING_COLORS); + } catch (e) { + console.error("Failed to parse BRANDING_COLORS", e); + } + } +} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index dd309c57..b84955dc 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -1,5 +1,4 @@ import { SidebarNavItem } from "@app/components/SidebarNav"; -import { build } from "@server/build"; import { Home, Settings, @@ -14,7 +13,9 @@ import { User, Globe, // Added from 'dev' branch MonitorUp, // Added from 'dev' branch - Zap + Server, + Zap, + CreditCard } from "lucide-react"; export type SidebarNavSection = { @@ -53,7 +54,8 @@ export const orgNavSections = ( { title: "sidebarClients", href: "/{orgId}/settings/clients", - icon: + icon: , + isBeta: true } ] : []), diff --git a/src/app/page.tsx b/src/app/page.tsx index 5c150c58..676889f0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,7 +12,6 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import { Layout } from "@app/components/Layout"; import { InitialSetupCompleteResponse } from "@server/routers/auth"; import { cookies } from "next/headers"; -import { build } from "@server/build"; export const dynamic = "force-dynamic"; @@ -29,10 +28,13 @@ export default async function Page(props: { const getUser = cache(verifySession); const user = await getUser({ skipCheckVerifyEmail: true }); - const setupRes = await internal.get< - AxiosResponse - >(`/auth/initial-setup-complete`, await authCookieHeader()); - const complete = setupRes.data.data.complete; + let complete = false; + try { + const setupRes = await internal.get< + AxiosResponse + >(`/auth/initial-setup-complete`, await authCookieHeader()); + complete = setupRes.data.data.complete; + } catch (e) {} if (!complete) { redirect("/auth/initial-setup"); } @@ -80,7 +82,10 @@ export default async function Page(props: { if (lastOrgExists) { redirect(`/${lastOrgCookie}`); } else { - const ownedOrg = orgs.find((org) => org.isOwner); + let ownedOrg = orgs.find((org) => org.isOwner); + if (!ownedOrg) { + ownedOrg = orgs[0]; + } if (ownedOrg) { redirect(`/${ownedOrg.orgId}`); } else { diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 00000000..c206249d --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,10 @@ +import type { MetadataRoute } from "next"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + disallow: "/" + } + }; +} diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index 06dd3300..d2854c0c 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -12,7 +12,7 @@ import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; export const metadata: Metadata = { - title: `Setup - Pangolin`, + title: `Setup - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, description: "" }; diff --git a/src/components/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx index 2efd9e7c..63a0b4bb 100644 --- a/src/components/AdminIdpDataTable.tsx +++ b/src/components/AdminIdpDataTable.tsx @@ -8,11 +8,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function IdpDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const router = useRouter(); const t = useTranslations(); @@ -29,6 +33,8 @@ export function IdpDataTable({ onAdd={() => { router.push("/admin/idp/create"); }} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx index 8849ba25..2db1415e 100644 --- a/src/components/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -39,8 +39,26 @@ export default function IdpTable({ idps }: Props) { const [selectedIdp, setSelectedIdp] = useState(null); const api = createApiClient(useEnvContext()); const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); const t = useTranslations(); + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteIdp = async (idpId: number) => { try { await api.delete(`/idp/${idpId}`); @@ -194,7 +212,12 @@ export default function IdpTable({ idps }: Props) { /> )} - + ); } diff --git a/src/components/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx index fecba7fb..b0f38587 100644 --- a/src/components/AdminUsersDataTable.tsx +++ b/src/components/AdminUsersDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function UsersDataTable({ title={t('userServer')} searchPlaceholder={t('userSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx index 8e75ff24..6bca4a74 100644 --- a/src/components/AdminUsersTable.tsx +++ b/src/components/AdminUsersTable.tsx @@ -46,6 +46,25 @@ export default function UsersTable({ users }: Props) { const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteUser = (id: string) => { api.delete(`/user/${id}`) .catch((e) => { @@ -168,7 +187,7 @@ export default function UsersTable({ users }: Props) {
{userRow.twoFactorEnabled || - userRow.twoFactorSetupRequested ? ( + userRow.twoFactorSetupRequested ? ( {t("enabled")} @@ -263,7 +282,12 @@ export default function UsersTable({ users }: Props) { /> )} - + ); } diff --git a/src/components/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx index 6ac8d68b..58ab9252 100644 --- a/src/components/ApiKeysDataTable.tsx +++ b/src/components/ApiKeysDataTable.tsx @@ -33,16 +33,20 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; addApiKey?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ApiKeysDataTable({ addApiKey, columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); - + return ( ({ searchColumn="name" onAdd={addApiKey} addButtonText={t('apiKeysAdd')} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index 99094651..adc150cf 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -43,6 +43,25 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteSite = (apiKeyId: string) => { api.delete(`/api-key/${apiKeyId}`) .catch((e) => { @@ -186,6 +205,8 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { addApiKey={() => { router.push(`/admin/api-keys/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/AutoLoginHandler.tsx b/src/components/AutoLoginHandler.tsx index c489a759..2391ece6 100644 --- a/src/components/AutoLoginHandler.tsx +++ b/src/components/AutoLoginHandler.tsx @@ -3,9 +3,8 @@ import { useEffect, useState } from "react"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { GenerateOidcUrlResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; -import { useRouter } from "next/navigation"; +import { redirect, useRouter } from "next/navigation"; import { Card, CardHeader, @@ -16,17 +15,20 @@ import { import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; import { useTranslations } from "next-intl"; +import { generateOidcUrlProxy } from "@app/actions/server"; type AutoLoginHandlerProps = { resourceId: number; skipToIdpId: number; redirectUrl: string; + orgId?: string; }; export default function AutoLoginHandler({ resourceId, skipToIdpId, - redirectUrl + redirectUrl, + orgId }: AutoLoginHandlerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -40,24 +42,39 @@ export default function AutoLoginHandler({ async function initiateAutoLogin() { setLoading(true); + let doRedirect: string | undefined; try { - const res = await api.post< - AxiosResponse - >(`/auth/idp/${skipToIdpId}/oidc/generate-url`, { - redirectUrl - }); + const response = await generateOidcUrlProxy( + skipToIdpId, + redirectUrl, + orgId + ); - if (res.data.data.redirectUrl) { - // Redirect to the IDP for authentication - window.location.href = res.data.data.redirectUrl; + if (response.error) { + setError(response.message); + setLoading(false); + return; + } + + const data = response.data; + const url = data?.redirectUrl; + if (url) { + doRedirect = url; } else { setError(t("autoLoginErrorNoRedirectUrl")); } - } catch (e) { + } catch (e: any) { console.error("Failed to generate OIDC URL:", e); - setError(formatAxiosError(e, t("autoLoginErrorGeneratingUrl"))); + setError( + t("autoLoginErrorGeneratingUrl", { + defaultValue: "An unexpected error occurred. Please try again." + }) + ); } finally { setLoading(false); + if (doRedirect) { + redirect(doRedirect); + } } } @@ -69,7 +86,9 @@ export default function AutoLoginHandler({ {t("autoLoginTitle")} - {t("autoLoginDescription")} + + {t("autoLoginDescription")} + {loading && ( diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index 34771333..25627e88 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -27,10 +27,16 @@ export default function BrandingLogo(props: BrandingLogoProps) { } if (lightOrDark === "light") { + if (env.branding.logo?.lightPath) { + return env.branding.logo.lightPath; + } return "/logo/word_mark_black.png"; } - return "/logo/word_mark_white.png"; + if (env.branding.logo?.darkPath) { + return env.branding.logo.darkPath; + } + return "/logo/word_mark_white.png"; } const path = getPath(); diff --git a/src/components/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx index 6242ba05..619f1fad 100644 --- a/src/components/ClientsDataTable.tsx +++ b/src/components/ClientsDataTable.tsx @@ -8,13 +8,17 @@ import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; addClient?: () => void; } export function ClientsDataTable({ columns, data, - addClient + addClient, + onRefresh, + isRefreshing }: DataTableProps) { return ( ({ searchPlaceholder="Search clients..." searchColumn="name" onAdd={addClient} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText="Add Client" /> ); diff --git a/src/components/ClientsTable.tsx b/src/components/ClientsTable.tsx index fc7c7c84..425b8395 100644 --- a/src/components/ClientsTable.tsx +++ b/src/components/ClientsTable.tsx @@ -25,6 +25,7 @@ import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; export type ClientRow = { id: number; @@ -53,6 +54,25 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { const [rows, setRows] = useState(clients); const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + const t = useTranslations(); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const deleteClient = (clientId: number) => { api.delete(`/client/${clientId}`) @@ -207,32 +227,32 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { return (
- - - - - - {/* */} - {/* */} - {/* View settings */} - {/* */} - {/* */} - { - setSelectedClient(clientRow); - setIsDeleteModalOpen(true); - }} - > - Delete - - - + + + + + + {/* */} + {/* */} + {/* View settings */} + {/* */} + {/* */} + { + setSelectedClient(clientRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + @@ -292,6 +312,8 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { addClient={() => { router.push(`/${orgId}/settings/clients/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 7ed31b62..b97e0eeb 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -81,9 +81,10 @@ export const ContainersSelector: FC = ({ <> diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 77fdea9c..64ca5ed8 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -43,7 +43,6 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import { useOrgContext } from "@app/hooks/useOrgContext"; -import { build } from "@server/build"; import { toASCII, toUnicode } from 'punycode'; @@ -119,12 +118,13 @@ export default function CreateDomainForm({ const t = useTranslations(); const { toast } = useToast(); const { org } = useOrgContext(); + const { env } = useEnvContext(); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", - type: build == "oss" ? "wildcard" : "ns" + type: "wildcard" } }); @@ -169,7 +169,7 @@ export default function CreateDomainForm({ }, [domainInputValue]); let domainOptions: any = []; - if (build == "enterprise" || build == "saas") { + /* if (build != "oss" && env.flags.usePangolinDns) { domainOptions = [ { id: "ns", @@ -182,15 +182,14 @@ export default function CreateDomainForm({ description: t("selectDomainTypeCnameDescription") } ]; - } else if (build == "oss") { - domainOptions = [ - { - id: "wildcard", - title: t("selectDomainTypeWildcardName"), - description: t("selectDomainTypeWildcardDescription") - } - ]; - } + } */ + domainOptions = [ + { + id: "wildcard", + title: t("selectDomainTypeWildcardName"), + description: t("selectDomainTypeWildcardDescription") + } + ]; return ( - {build == "saas" || - (build == "enterprise" && ( + {/*build != "oss" && env.flags.usePangolinDns && ( @@ -572,7 +570,7 @@ export default function CreateDomainForm({ )} - ))} + )*/} diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index e3ba3f17..b38bab91 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -388,6 +388,7 @@ export default function CreateShareLinkForm({ field.onChange } defaultValue={field.value.toString()} + disabled={neverExpire} > @@ -423,6 +424,7 @@ export default function CreateShareLinkForm({ ( @@ -474,7 +476,9 @@ export default function CreateShareLinkForm({

+
+
- +

{getSubtitle()}

diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index c14374d5..66267bdf 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -32,10 +32,10 @@ import { createApiClient, formatAxiosError } from "@/lib/api"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; import { ListDomainsResponse } from "@server/routers/domain/listDomains"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; import { AxiosResponse } from "axios"; import { cn } from "@/lib/cn"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { sanitizeInputRaw, @@ -92,6 +92,10 @@ export default function DomainPicker2({ const api = createApiClient({ env }); const t = useTranslations(); + if (!env.flags.usePangolinDns) { + hideFreeDomain = true; + } + const [subdomainInput, setSubdomainInput] = useState(""); const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); @@ -155,7 +159,10 @@ export default function DomainPicker2({ fullDomain: firstOrgDomain.baseDomain, baseDomain: firstOrgDomain.baseDomain }); - } else if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { + }/* else if ( + (build === "saas" || build === "enterprise") && + !hideFreeDomain + ) { // If no organization domains, select the provided domain option const domainOptionText = build === "enterprise" @@ -167,7 +174,7 @@ export default function DomainPicker2({ type: "provided-search" }; setSelectedBaseDomain(freeDomainOption); - } + }*/ } } catch (error) { console.error("Failed to load organization domains:", error); @@ -198,7 +205,21 @@ export default function DomainPicker2({ .toLowerCase() .replace(/\./g, "-") .replace(/[^a-z0-9-]/g, "") - .replace(/-+/g, "-"); + .replace(/-+/g, "-") // Replace multiple consecutive dashes with single dash + .replace(/^-|-$/g, ""); // Remove leading/trailing dashes + + /* if (build != "oss") { + const response = await api.get< + AxiosResponse + >( + `/domain/check-namespace-availability?subdomain=${encodeURIComponent(checkSubdomain)}` + ); + + if (response.status === 200) { + const { options } = response.data.data; + setAvailableOptions(options); + } + }*/ } catch (error) { console.error("Failed to check domain availability:", error); setAvailableOptions([]); @@ -248,7 +269,7 @@ export default function DomainPicker2({ }); }); - if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { + /* if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { const domainOptionText = build === "enterprise" ? t("domainPickerProvidedDomain") @@ -258,7 +279,7 @@ export default function DomainPicker2({ domain: domainOptionText, type: "provided-search" }); - } + }*/ return options; }; @@ -272,13 +293,16 @@ export default function DomainPicker2({ toast({ variant: "destructive", title: t("domainPickerInvalidSubdomain"), - description: t("domainPickerInvalidSubdomainRemoved", { sub }), + description: t("domainPickerInvalidSubdomainRemoved", { sub }) }); return ""; } const ok = validateByDomainType(sanitized, { - type: base.type === "provided-search" ? "provided-search" : "organization", + type: + base.type === "provided-search" + ? "provided-search" + : "organization", domainType: base.domainType }); @@ -286,7 +310,10 @@ export default function DomainPicker2({ toast({ variant: "destructive", title: t("domainPickerInvalidSubdomain"), - description: t("domainPickerInvalidSubdomainCannotMakeValid", { sub, domain: base.domain }), + description: t("domainPickerInvalidSubdomainCannotMakeValid", { + sub, + domain: base.domain + }) }); return ""; } @@ -294,7 +321,10 @@ export default function DomainPicker2({ if (sub !== sanitized) { toast({ title: t("domainPickerSubdomainSanitized"), - description: t("domainPickerSubdomainCorrected", { sub, sanitized }), + description: t("domainPickerSubdomainCorrected", { + sub, + sanitized + }) }); } @@ -365,7 +395,8 @@ export default function DomainPicker2({ onDomainChange?.({ domainId: option.domainId || "", domainNamespaceId: option.domainNamespaceId, - type: option.type === "provided-search" ? "provided" : "organization", + type: + option.type === "provided-search" ? "provided" : "organization", subdomain: sub || undefined, fullDomain, baseDomain: option.domain @@ -389,12 +420,16 @@ export default function DomainPicker2({ }); }; - const isSubdomainValid = selectedBaseDomain && subdomainInput - ? validateByDomainType(subdomainInput, { - type: selectedBaseDomain.type === "provided-search" ? "provided-search" : "organization", - domainType: selectedBaseDomain.domainType - }) - : true; + const isSubdomainValid = + selectedBaseDomain && subdomainInput + ? validateByDomainType(subdomainInput, { + type: + selectedBaseDomain.type === "provided-search" + ? "provided-search" + : "organization", + domainType: selectedBaseDomain.domainType + }) + : true; const showSubdomainInput = selectedBaseDomain && @@ -415,7 +450,6 @@ export default function DomainPicker2({ const hasMoreProvided = sortedAvailableOptions.length > providedDomainsShown; - return (
@@ -434,16 +468,16 @@ export default function DomainPicker2({ showProvidedDomainSearch ? "" : showSubdomainInput - ? "" - : t("domainPickerNotAvailableForCname") + ? "" + : t("domainPickerNotAvailableForCname") } disabled={ !showSubdomainInput && !showProvidedDomainSearch } className={cn( !isSubdomainValid && - subdomainInput && - "border-red-500 focus:border-red-500" + subdomainInput && + "border-red-500 focus:border-red-500" )} onChange={(e) => { if (showProvidedDomainSearch) { @@ -453,11 +487,13 @@ export default function DomainPicker2({ } }} /> - {showSubdomainInput && subdomainInput && !isValidSubdomainStructure(subdomainInput) && ( -

- {t("domainPickerInvalidSubdomainStructure")} -

- )} + {showSubdomainInput && + subdomainInput && + !isValidSubdomainStructure(subdomainInput) && ( +

+ {t("domainPickerInvalidSubdomainStructure")} +

+ )} {showSubdomainInput && !subdomainInput && (

{t("domainPickerEnterSubdomainOrLeaveBlank")} @@ -483,7 +519,7 @@ export default function DomainPicker2({ {selectedBaseDomain ? (

{selectedBaseDomain.type === - "organization" ? null : ( + "organization" ? null : ( )} @@ -557,8 +593,12 @@ export default function DomainPicker2({ {orgDomain.type.toUpperCase()}{" "} •{" "} {orgDomain.verified - ? t("domainPickerVerified") - : t("domainPickerUnverified")} + ? t( + "domainPickerVerified" + ) + : t( + "domainPickerUnverified" + )}
- {(build === "saas" || - build === "enterprise") && !hideFreeDomain && ( + {/*(build === "saas" || + build === "enterprise") && + !hideFreeDomain && ( - )} + )*/} )} - {(build === "saas" || - build === "enterprise") && !hideFreeDomain && ( + {/*(build === "saas" || build === "enterprise") && + !hideFreeDomain && ( @@ -602,9 +645,13 @@ export default function DomainPicker2({ id: "provided-search", domain: build === - "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"), + "enterprise" + ? t( + "domainPickerProvidedDomain" + ) + : t( + "domainPickerFreeProvidedDomain" + ), type: "provided-search" }) } @@ -615,9 +662,14 @@ export default function DomainPicker2({
- {build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain")} + {build === + "enterprise" + ? t( + "domainPickerProvidedDomain" + ) + : t( + "domainPickerFreeProvidedDomain" + )} {t( @@ -637,13 +689,22 @@ export default function DomainPicker2({ - )} + )*/}
+ {/*showProvidedDomainSearch && build === "saas" && ( + + + + {t("domainPickerNotWorkSelfHosted")} + + + )*/} + {showProvidedDomainSearch && (
{isChecking && ( @@ -693,7 +754,7 @@ export default function DomainPicker2({ htmlFor={option.domainNamespaceId} data-state={ selectedProvidedDomain?.domainNamespaceId === - option.domainNamespaceId + option.domainNamespaceId ? "checked" : "unchecked" } diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx new file mode 100644 index 00000000..9cbddd70 --- /dev/null +++ b/src/components/GenerateLicenseKeyForm.tsx @@ -0,0 +1,1084 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { Checkbox } from "@app/components/ui/checkbox"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AxiosResponse } from "axios"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; +import { useTranslations } from "next-intl"; +import React from "react"; +import { StrategySelect, StrategyOption } from "./StrategySelect"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; +import { InfoIcon, Check } from "lucide-react"; +import { useUserContext } from "@app/hooks/useUserContext"; + +type FormProps = { + open: boolean; + setOpen: (open: boolean) => void; + orgId: string; + onGenerated?: () => void; +}; + +export default function GenerateLicenseKeyForm({ + open, + setOpen, + orgId, + onGenerated +}: FormProps) { + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const { user } = useUserContext(); + + const [loading, setLoading] = useState(false); + const [generatedKey, setGeneratedKey] = useState(null); + + // Personal form schema + const personalFormSchema = z.object({ + email: z.string().email(), + firstName: z.string().min(1), + lastName: z.string().min(1), + primaryUse: z.string().min(1), + country: z.string().min(1), + phoneNumber: z.string().optional(), + agreedToTerms: z.boolean().refine((val) => val === true), + complianceConfirmed: z.boolean().refine((val) => val === true) + }); + + // Business form schema + const businessFormSchema = z.object({ + email: z.string().email(), + firstName: z.string().min(1), + lastName: z.string().min(1), + jobTitle: z.string().min(1), + primaryUse: z.string().min(1), + industry: z.string().min(1), + prospectiveUsers: z.coerce.number().optional(), + prospectiveSites: z.coerce.number().optional(), + companyName: z.string().min(1), + countryOfResidence: z.string().min(1), + stateProvinceRegion: z.string().min(1), + postalZipCode: z.string().min(1), + companyWebsite: z.string().optional(), + companyPhoneNumber: z.string().optional(), + agreedToTerms: z.boolean().refine((val) => val === true), + complianceConfirmed: z.boolean().refine((val) => val === true) + }); + + type PersonalFormData = z.infer; + type BusinessFormData = z.infer; + + const [useCaseType, setUseCaseType] = useState( + undefined + ); + + // Personal form + const personalForm = useForm({ + resolver: zodResolver(personalFormSchema), + defaultValues: { + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + country: "", + phoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + } + }); + + // Business form + const businessForm = useForm({ + resolver: zodResolver(businessFormSchema), + defaultValues: { + email: user?.email || "", + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + companyName: "", + countryOfResidence: "", + stateProvinceRegion: "", + postalZipCode: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + } + }); + + // Reset form when dialog opens + React.useEffect(() => { + if (open) { + resetForm(); + setGeneratedKey(null); + } + }, [open]); + + function resetForm() { + personalForm.reset({ + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + country: "", + phoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + + businessForm.reset({ + email: user?.email || "", + firstName: "", + lastName: "", + jobTitle: "", + primaryUse: "", + industry: "", + prospectiveUsers: undefined, + prospectiveSites: undefined, + companyName: "", + countryOfResidence: "", + stateProvinceRegion: "", + postalZipCode: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + } + + const useCaseOptions: StrategyOption<"personal" | "business">[] = [ + { + id: "personal", + title: t("generateLicenseKeyForm.useCaseOptions.personal.title"), + description: ( +
+

+ {t( + "generateLicenseKeyForm.useCaseOptions.personal.description" + )} +

+
    +
  • + + + Home-lab enthusiasts and self-hosting hobbyists + +
  • +
  • + + + Personal projects, learning, and experimentation + +
  • +
  • + + + Individual developers and tech enthusiasts + +
  • +
+
+ ) + }, + { + id: "business", + title: t("generateLicenseKeyForm.useCaseOptions.business.title"), + description: ( +
+

+ {t( + "generateLicenseKeyForm.useCaseOptions.business.description" + )} +

+
    +
  • + + + Companies, startups, and organizations + +
  • +
  • + + + Professional services and client work + +
  • +
  • + + + Revenue-generating or commercial use cases + +
  • +
+
+ ) + } + ]; + + const submitLicenseRequest = async (payload: any) => { + setLoading(true); + try { + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license`, payload); + + if (response.data.data?.licenseKey?.licenseKey) { + setGeneratedKey(response.data.data.licenseKey.licenseKey); + onGenerated?.(); + toast({ + title: t("generateLicenseKeyForm.toasts.success.title"), + description: t( + "generateLicenseKeyForm.toasts.success.description" + ), + variant: "default" + }); + } + } catch (e) { + console.error(e); + toast({ + title: t("generateLicenseKeyForm.toasts.error.title"), + description: formatAxiosError( + e, + t("generateLicenseKeyForm.toasts.error.description") + ), + variant: "destructive" + }); + } + setLoading(false); + }; + + const onSubmitPersonal = async (values: PersonalFormData) => { + const payload = { + email: values.email, + useCaseType: "personal", + personal: { + firstName: values.firstName, + lastName: values.lastName, + aboutYou: { + primaryUse: values.primaryUse + }, + personalInfo: { + country: values.country, + phoneNumber: values.phoneNumber || "" + } + }, + business: undefined, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed + } + }; + + await submitLicenseRequest(payload); + }; + + const onSubmitBusiness = async (values: BusinessFormData) => { + const payload = { + email: values.email, + useCaseType: "business", + personal: undefined, + business: { + firstName: values.firstName, + lastName: values.lastName, + jobTitle: values.jobTitle, + aboutYou: { + primaryUse: values.primaryUse, + industry: values.industry, + prospectiveUsers: values.prospectiveUsers || undefined, + prospectiveSites: values.prospectiveSites || undefined + }, + companyInfo: { + companyName: values.companyName, + countryOfResidence: values.countryOfResidence, + stateProvinceRegion: values.stateProvinceRegion, + postalZipCode: values.postalZipCode, + companyWebsite: values.companyWebsite || "", + companyPhoneNumber: values.companyPhoneNumber || "" + } + }, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed + } + }; + + await submitLicenseRequest(payload); + }; + + const handleClose = () => { + setOpen(false); + setGeneratedKey(null); + resetForm(); + }; + + return ( + + + + {t("generateLicenseKey")} + + {t( + "generateLicenseKeyForm.steps.emailLicenseType.description" + )} + + + +
+ {generatedKey ? ( +
+ {useCaseType === "business" && ( + + + {t( + "generateLicenseKeyForm.alerts.trialPeriodInformation.title" + )} + + + {t( + "generateLicenseKeyForm.alerts.trialPeriodInformation.description" + )} + + + )} + + +
+ ) : ( + <> + + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.title" + )} + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.description" + ) + .split( + "Fossorial Commercial License Terms" + ) + .map((part, index) => ( + + {part} + {index === 0 && ( + + Fossorial Commercial + License Terms + + )} + + ))} + + + +
+ +
+ { + setUseCaseType(value); + resetForm(); + }} + cols={2} + /> +
+
+ + {useCaseType === "personal" && ( +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.firstName" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.lastName" + )} + + + + + + + )} + /> +
+ + ( + + + {t( + "generateLicenseKeyForm.form.primaryUseQuestion" + )} + + + + + + + )} + /> + +
+
+ ( + + + {t( + "generateLicenseKeyForm.form.country" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.phoneNumberOptional" + )} + + + + + + + )} + /> +
+
+ +
+ ( + + + + +
+ +
+ {t( + "signUpTerms.IAgreeToThe" + )}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t( + "signUpTerms.and" + )}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + + ( + + + + +
+ +
+ {t( + "generateLicenseKeyForm.form.complianceConfirmation" + )}{" "} + See + license + details:{" "} + + https://digpangolin.com/fcl.html + +
+
+ +
+
+ )} + /> +
+ + + )} + + {useCaseType === "business" && ( +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.firstName" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.lastName" + )} + + + + + + + )} + /> +
+ + ( + + + {t( + "generateLicenseKeyForm.form.jobTitle" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.primaryUseQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.industryQuestion" + )} + + + + + + + )} + /> + +
+ ( + + + {t( + "generateLicenseKeyForm.form.prospectiveUsersQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.prospectiveSitesQuestion" + )} + + + + + + + )} + /> +
+ + ( + + + {t( + "generateLicenseKeyForm.form.companyName" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.countryOfResidence" + )} + + + + + + + )} + /> + +
+ ( + + + {t( + "generateLicenseKeyForm.form.stateProvinceRegion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.postalZipCode" + )} + + + + + + + )} + /> +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.companyWebsite" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.companyPhoneNumber" + )} + + + + + + + )} + /> +
+ +
+ ( + + + + +
+ +
+ {t( + "signUpTerms.IAgreeToThe" + )}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t( + "signUpTerms.and" + )}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + + ( + + + + +
+ +
+ {t( + "generateLicenseKeyForm.form.complianceConfirmation" + )}{" "} + See + license + details:{" "} + + https://digpangolin.com/fcl.html + +
+
+ +
+
+ )} + /> +
+ + + )} + + )} +
+
+ + + + + + {!generatedKey && useCaseType === "personal" && ( + + )} + + {!generatedKey && useCaseType === "business" && ( + + )} + +
+
+ ); +} diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx new file mode 100644 index 00000000..835bb70d --- /dev/null +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "./ui/button"; +import { ArrowUpDown } from "lucide-react"; +import CopyToClipboard from "./CopyToClipboard"; +import { Badge } from "./ui/badge"; +import moment from "moment"; +import { DataTable } from "./ui/data-table"; +import { GeneratedLicenseKey } from "@server/routers/generatedLicense/types"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import GenerateLicenseKeyForm from "./GenerateLicenseKeyForm"; + +type GnerateLicenseKeysTableProps = { + licenseKeys: GeneratedLicenseKey[]; + orgId: string; +}; + +function obfuscateLicenseKey(key: string): string { + if (key.length <= 8) return key; + const firstPart = key.substring(0, 4); + const lastPart = key.substring(key.length - 4); + return `${firstPart}••••••••••••••••••••${lastPart}`; +} + +export default function GenerateLicenseKeysTable({ + licenseKeys, + orgId +}: GnerateLicenseKeysTableProps) { + const t = useTranslations(); + const router = useRouter(); + + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const [isRefreshing, setIsRefreshing] = useState(false); + const [showGenerateForm, setShowGenerateForm] = useState(false); + + const handleLicenseGenerated = () => { + // Refresh the data after license is generated + refreshData(); + }; + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "licenseKey", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const licenseKey = row.original.licenseKey; + return ( + + ); + } + }, + { + accessorKey: "instanceName", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return row.original.instanceName || "-"; + } + }, + { + accessorKey: "valid", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return row.original.isValid ? ( + {t("yes")} + ) : ( + {t("no")} + ); + } + }, + { + accessorKey: "type", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const tier = row.original.tier; + return tier === "enterprise" + ? t("licenseTierEnterprise") + : t("licenseTierPersonal"); + } + }, + { + accessorKey: "terminateAt", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const termianteAt = row.original.expiresAt; + return moment(termianteAt).format("lll"); + } + } + ]; + + return ( + <> + { + setShowGenerateForm(true); + }} + /> + + + + ); +} diff --git a/src/components/HealthCheckDialog.tsx b/src/components/HealthCheckDialog.tsx new file mode 100644 index 00000000..6fa36a5b --- /dev/null +++ b/src/components/HealthCheckDialog.tsx @@ -0,0 +1,580 @@ +"use client"; + +import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { HeadersInput } from "@app/components/HeadersInput"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@/components/Credenza"; +import { toast } from "@/hooks/useToast"; +import { useTranslations } from "next-intl"; + +type HealthCheckConfig = { + hcEnabled: boolean; + hcPath: string; + hcMethod: string; + hcInterval: number; + hcTimeout: number; + hcStatus: number | null; + hcHeaders?: { name: string; value: string }[] | null; + hcScheme?: string; + hcHostname: string; + hcPort: number; + hcFollowRedirects: boolean; + hcMode: string; + hcUnhealthyInterval: number; +}; + +type HealthCheckDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + targetId: number; + targetAddress: string; + targetMethod?: string; + initialConfig?: Partial; + onChanges: (config: HealthCheckConfig) => Promise; +}; + +export default function HealthCheckDialog({ + open, + setOpen, + targetId, + targetAddress, + targetMethod, + initialConfig, + onChanges +}: HealthCheckDialogProps) { + const t = useTranslations(); + + const healthCheckSchema = z.object({ + hcEnabled: z.boolean(), + hcPath: z.string().min(1, { message: t("healthCheckPathRequired") }), + hcMethod: z + .string() + .min(1, { message: t("healthCheckMethodRequired") }), + hcInterval: z + .number() + .int() + .positive() + .min(5, { message: t("healthCheckIntervalMin") }), + hcTimeout: z + .number() + .int() + .positive() + .min(1, { message: t("healthCheckTimeoutMin") }), + hcStatus: z.number().int().positive().min(100).optional().nullable(), + hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), + hcScheme: z.string().optional(), + hcHostname: z.string(), + hcPort: z.number().positive().gt(0).lte(65535), + hcFollowRedirects: z.boolean(), + hcMode: z.string(), + hcUnhealthyInterval: z.number().int().positive().min(5) + }); + + const form = useForm>({ + resolver: zodResolver(healthCheckSchema), + defaultValues: {} + }); + + useEffect(() => { + if (!open) return; + + // Determine default scheme from target method + const getDefaultScheme = () => { + if (initialConfig?.hcScheme) { + return initialConfig.hcScheme; + } + // Default to target method if it's http or https, otherwise default to http + if (targetMethod === "https") { + return "https"; + } + return "http"; + }; + + form.reset({ + hcEnabled: initialConfig?.hcEnabled, + hcPath: initialConfig?.hcPath, + hcMethod: initialConfig?.hcMethod, + hcInterval: initialConfig?.hcInterval, + hcTimeout: initialConfig?.hcTimeout, + hcStatus: initialConfig?.hcStatus, + hcHeaders: initialConfig?.hcHeaders, + hcScheme: getDefaultScheme(), + hcHostname: initialConfig?.hcHostname, + hcPort: initialConfig?.hcPort, + hcFollowRedirects: initialConfig?.hcFollowRedirects, + hcMode: initialConfig?.hcMode, + hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval + }); + }, [open]); + + const watchedEnabled = form.watch("hcEnabled"); + + const handleFieldChange = async (fieldName: string, value: any) => { + try { + const currentValues = form.getValues(); + const updatedValues = { ...currentValues, [fieldName]: value }; + await onChanges({ + ...updatedValues, + hcStatus: updatedValues.hcStatus || null + }); + } catch (error) { + toast({ + title: t("healthCheckError"), + description: t("healthCheckErrorDescription"), + variant: "destructive" + }); + } + }; + + return ( + + + + {t("configureHealthCheck")} + + {t("configureHealthCheckDescription", { + target: targetAddress + })} + + + +
+ + {/* Enable Health Checks */} + ( + +
+ + {t("enableHealthChecks")} + + + {t( + "enableHealthChecksDescription" + )} + +
+ + { + field.onChange(value); + handleFieldChange( + "hcEnabled", + value + ); + }} + /> + +
+ )} + /> + + {watchedEnabled && ( +
+
+ ( + + + {t("healthScheme")} + + + + + )} + /> + ( + + + {t("healthHostname")} + + + { + field.onChange( + e + ); + handleFieldChange( + "hcHostname", + e.target + .value + ); + }} + /> + + + + )} + /> + ( + + + {t("healthPort")} + + + { + const value = + parseInt( + e.target + .value + ); + field.onChange( + value + ); + handleFieldChange( + "hcPort", + value + ); + }} + /> + + + + )} + /> + ( + + + {t("healthCheckPath")} + + + { + field.onChange( + e + ); + handleFieldChange( + "hcPath", + e.target + .value + ); + }} + /> + + + + )} + /> +
+ + {/* HTTP Method */} + ( + + + {t("httpMethod")} + + + + + )} + /> + + {/* Check Interval, Timeout, and Retry Attempts */} +
+ ( + + + {t( + "healthyIntervalSeconds" + )} + + + { + const value = + parseInt( + e.target + .value + ); + field.onChange( + value + ); + handleFieldChange( + "hcInterval", + value + ); + }} + /> + + + + )} + /> + + ( + + + {t( + "unhealthyIntervalSeconds" + )} + + + { + const value = + parseInt( + e.target + .value + ); + field.onChange( + value + ); + handleFieldChange( + "hcUnhealthyInterval", + value + ); + }} + /> + + + + )} + /> + + ( + + + {t("timeoutSeconds")} + + + { + const value = + parseInt( + e.target + .value + ); + field.onChange( + value + ); + handleFieldChange( + "hcTimeout", + value + ); + }} + /> + + + + )} + /> + + + {t("timeIsInSeconds")} + +
+ + {/* Expected Response Codes */} + ( + + + {t("expectedResponseCodes")} + + + { + const value = + parseInt( + e.target + .value + ); + field.onChange( + value + ); + handleFieldChange( + "hcStatus", + value + ); + }} + /> + + + {t( + "expectedResponseCodesDescription" + )} + + + + )} + /> + + {/* Custom Headers */} + ( + + + {t("customHeaders")} + + + { + field.onChange(value); + handleFieldChange( + "hcHeaders", + value + ); + }} + rows={4} + /> + + + {t( + "customHeadersDescription" + )} + + + + )} + /> +
+ )} + + +
+ + + +
+
+ ); +} diff --git a/src/components/InvitationsDataTable.tsx b/src/components/InvitationsDataTable.tsx index 396a3c20..d73ad2ca 100644 --- a/src/components/InvitationsDataTable.tsx +++ b/src/components/InvitationsDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from 'next-intl'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function InvitationsDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function InvitationsDataTable({ title={t('invite')} searchPlaceholder={t('inviteSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/InvitationsTable.tsx b/src/components/InvitationsTable.tsx index a97220f2..900003d7 100644 --- a/src/components/InvitationsTable.tsx +++ b/src/components/InvitationsTable.tsx @@ -19,6 +19,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import moment from "moment"; +import { useRouter } from "next/navigation"; export type InvitationRow = { id: string; @@ -45,6 +46,25 @@ export default function InvitationsTable({ const api = createApiClient(useEnvContext()); const { org } = useOrgContext(); + const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -185,7 +205,12 @@ export default function InvitationsTable({ }} /> - + ); } diff --git a/src/components/InviteStatusCard.tsx b/src/components/InviteStatusCard.tsx index 6d7db4dc..d394bd57 100644 --- a/src/components/InviteStatusCard.tsx +++ b/src/components/InviteStatusCard.tsx @@ -1,47 +1,119 @@ "use client"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, - CardTitle, + CardTitle } from "@app/components/ui/card"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { XCircle } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; +import { AcceptInviteResponse, GetUserResponse } from "@server/routers/user"; +import { Loader2 } from "lucide-react"; type InviteStatusCardProps = { - type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; - token: string; + user: GetUserResponse | null; + tokenParam: string; + inviteId: string; + inviteToken: string; email?: string; }; export default function InviteStatusCard({ - type, - token, + inviteId, email, + user, + tokenParam, + inviteToken }: InviteStatusCardProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); const t = useTranslations(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [type, setType] = useState< + "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in" + >("rejected"); + + useEffect(() => { + async function init() { + let error = ""; + const res = await api + .post>(`/invite/accept`, { + inviteId, + token: inviteToken + }) + .catch((e) => { + error = formatAxiosError(e); + console.log("Error accepting invite:", error); + setError(error); + // console.error(e); + }); + + if (res && res.status === 200) { + router.push(`/${res.data.data.orgId}`); + return; + } + + function cardType() { + if (error.includes("Invite is not for this user")) { + return "wrong_user"; + } else if ( + error.includes( + "User does not exist. Please create an account first." + ) + ) { + return "user_does_not_exist"; + } else if ( + error.includes("You must be logged in to accept an invite") + ) { + return "not_logged_in"; + } else { + return "rejected"; + } + } + + const type = cardType(); + setType(type); + + if (!user && type === "user_does_not_exist") { + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${tokenParam}`; + router.push(redirectUrl); + } else if (!user && type === "not_logged_in") { + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${tokenParam}`; + router.push(redirectUrl); + } else { + setLoading(false); + } + } + + init(); + }, []); + async function goToLogin() { await api.post("/auth/logout", {}); - const redirectUrl = email - ? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` - : `/auth/login?redirect=/invite?token=${token}`; + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${tokenParam}`; router.push(redirectUrl); } async function goToSignup() { await api.post("/auth/logout", {}); - const redirectUrl = email - ? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` - : `/auth/signup?redirect=/invite?token=${token}`; + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${tokenParam}`; router.push(redirectUrl); } @@ -50,35 +122,27 @@ export default function InviteStatusCard({ return (

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

    -
  • {t('inviteErrorExpired')}
  • -
  • {t('inviteErrorRevoked')}
  • -
  • {t('inviteErrorTypo')}
  • +
  • {t("inviteErrorExpired")}
  • +
  • {t("inviteErrorRevoked")}
  • +
  • {t("inviteErrorTypo")}
); } else if (type === "wrong_user") { return (
-

- {t('inviteErrorUser')} -

-

- {t('inviteLoginUser')} -

+

{t("inviteErrorUser")}

+

{t("inviteLoginUser")}

); } else if (type === "user_does_not_exist") { return (
-

- {t('inviteErrorNoUser')} -

-

- {t('inviteCreateUser')} -

+

{t("inviteErrorNoUser")}

+

{t("inviteCreateUser")}

); } @@ -92,37 +156,43 @@ export default function InviteStatusCard({ router.push("/"); }} > - {t('goHome')} + {t("goHome")} ); } else if (type === "wrong_user") { return ( - + ); } else if (type === "user_does_not_exist") { - return ; + return ; } } return ( -
+
- {/*
-
*/} - {t('inviteNotAccepted')} + {loading ? t("checkingInvite") : t("inviteNotAccepted")}
- {renderBody()} + + {loading && ( +
+
+ + {t("loading")} +
+
+ )} + {!loading && renderBody()} +
- - {renderFooter()} - + {!loading && ( + + {renderFooter()} + + )}
); diff --git a/src/components/LayoutHeader.tsx b/src/components/LayoutHeader.tsx index 2584b259..31a77ad2 100644 --- a/src/components/LayoutHeader.tsx +++ b/src/components/LayoutHeader.tsx @@ -1,11 +1,12 @@ "use client"; import React, { useEffect, useState } from "react"; -import Image from "next/image"; import Link from "next/link"; import ProfileIcon from "@app/components/ProfileIcon"; import ThemeSwitcher from "@app/components/ThemeSwitcher"; import { useTheme } from "next-themes"; +import BrandingLogo from "./BrandingLogo"; +import { useEnvContext } from "@app/hooks/useEnvContext"; interface LayoutHeaderProps { showTopBar: boolean; @@ -14,6 +15,10 @@ interface LayoutHeaderProps { export function LayoutHeader({ showTopBar }: LayoutHeaderProps) { const { theme } = useTheme(); const [path, setPath] = useState(""); + const { env } = useEnvContext(); + + const logoWidth = env.branding.logo?.navbar?.width || 98; + const logoHeight = env.branding.logo?.navbar?.height || 32; useEffect(() => { function getPath() { @@ -44,16 +49,14 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
- {path && ( - Pangolin - )} + + {/* {build === "saas" && ( + Cloud Beta + )} */}
{showTopBar && ( diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index 2ce8996b..7f458889 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -5,7 +5,15 @@ import { SidebarNav } from "@app/components/SidebarNav"; import { OrgSelector } from "@app/components/OrgSelector"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; -import { ExternalLink, Server, BookOpenText, Zap } from "lucide-react"; +import { + ExternalLink, + Server, + BookOpenText, + Zap, + CreditCard, + FileText, + TicketCheck +} from "lucide-react"; import { FaDiscord, FaGithub } from "react-icons/fa"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -19,7 +27,6 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; -import { build } from "@server/build"; interface LayoutSidebarProps { orgId?: string; @@ -54,6 +61,16 @@ export function LayoutSidebar({ setSidebarStateCookie(isSidebarCollapsed); }, [isSidebarCollapsed]); + function loadFooterLinks(): { text: string; href?: string }[] | undefined { + if (env.branding.footer) { + try { + return JSON.parse(env.branding.footer); + } catch (e) { + console.error("Failed to parse BRANDING_FOOTER", e); + } + } + } + return (
-
+ +
{!isSidebarCollapsed && (
-
- - {t("communityEdition")} - - -
-
- - {t("documentation")} - - -
-
- - Discord - - -
- {env?.app?.version && ( -
- - v{env.app.version} - - -
+ {loadFooterLinks() ? ( + <> + {loadFooterLinks()!.map((link, index) => ( +
+ {link.href ? ( +
+ + {link.text} + + +
+ ) : ( +
+ {link.text} +
+ )} +
+ ))} + + ) : ( + <> +
+ + {t("communityEdition")} + + +
+ {env?.app?.version && ( +
+ + v{env.app.version} + + +
+ )} + )}
)} @@ -167,7 +194,7 @@ export function LayoutSidebar({ onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed) } - className="cursor-pointer absolute -right-2.5 top-1/2 transform -translate-y-1/2 w-2 h-8 rounded-full flex items-center justify-center transition-all duration-200 ease-in-out hover:scale-110 group z-[60]" + className="cursor-pointer absolute -right-2.5 top-1/2 transform -translate-y-1/2 w-2 h-8 rounded-full flex items-center justify-center transition-all duration-200 ease-in-out hover:scale-110 group z-1" aria-label={ isSidebarCollapsed ? "Expand sidebar" diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index bef788d0..32bde9c3 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; @@ -22,13 +22,9 @@ import { CardTitle } from "@app/components/ui/card"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { LoginResponse } from "@server/routers/auth"; import { useRouter } from "next/navigation"; -import { AxiosResponse } from "axios"; -import { formatAxiosError } from "@app/lib/api"; import { LockIcon, FingerprintIcon } from "lucide-react"; import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; import { InputOTP, InputOTPGroup, @@ -42,6 +38,14 @@ import { GenerateOidcUrlResponse } from "@server/routers/idp"; import { Separator } from "./ui/separator"; import { useTranslations } from "next-intl"; import { startAuthentication } from "@simplewebauthn/browser"; +import { + generateOidcUrlProxy, + loginProxy, + securityKeyStartProxy, + securityKeyVerifyProxy +} from "@app/actions/server"; +import { redirect as redirectTo } from "next/navigation"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type LoginFormIDP = { idpId: number; @@ -53,13 +57,18 @@ type LoginFormProps = { redirect?: string; onLogin?: () => void | Promise; idps?: LoginFormIDP[]; + orgId?: string; }; -export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { +export default function LoginForm({ + redirect, + onLogin, + idps, + orgId +}: LoginFormProps) { const router = useRouter(); const { env } = useEnvContext(); - const api = createApiClient({ env }); const [error, setError] = useState(null); @@ -70,6 +79,10 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { const [showSecurityKeyPrompt, setShowSecurityKeyPrompt] = useState(false); const t = useTranslations(); + const currentHost = + typeof window !== "undefined" ? window.location.hostname : ""; + const expectedHost = new URL(env.app.dashboardUrl).host; + const isExpectedHost = currentHost === expectedHost; const formSchema = z.object({ email: z.string().email({ message: t("emailInvalid") }), @@ -102,39 +115,39 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { try { // Start WebAuthn authentication without email - const startRes = await api.post( - "/auth/security-key/authenticate/start", - {} - ); + const startResponse = await securityKeyStartProxy({}); - if (!startRes) { - setError( - t("securityKeyAuthError", { - defaultValue: - "Failed to start security key authentication" - }) - ); + if (startResponse.error) { + setError(startResponse.message); return; } - const { tempSessionId, ...options } = startRes.data.data; + const { tempSessionId, ...options } = startResponse.data!; // Perform WebAuthn authentication try { - const credential = await startAuthentication(options); + const credential = await startAuthentication({ + optionsJSON: { + ...options, + userVerification: options.userVerification as + | "required" + | "preferred" + | "discouraged" + } + }); // Verify authentication - const verifyRes = await api.post( - "/auth/security-key/authenticate/verify", + const verifyResponse = await securityKeyVerifyProxy( { credential }, - { - headers: { - "X-Temp-Session-Id": tempSessionId - } - } + tempSessionId ); - if (verifyRes) { + if (verifyResponse.error) { + setError(verifyResponse.message); + return; + } + + if (verifyResponse.success) { if (onLogin) { await onLogin(); } @@ -173,26 +186,13 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { } } } catch (e: any) { - if (e.isAxiosError) { - setError( - formatAxiosError( - e, - t("securityKeyAuthError", { - defaultValue: - "Failed to authenticate with security key" - }) - ) - ); - } else { - console.error(e); - setError( - e.message || - t("securityKeyAuthError", { - defaultValue: - "Failed to authenticate with security key" - }) - ); - } + console.error(e); + setError( + t("securityKeyAuthError", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); } finally { setLoading(false); setShowSecurityKeyPrompt(false); @@ -208,30 +208,48 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { setShowSecurityKeyPrompt(false); try { - const res = await api.post>( - "/auth/login", - { - email, - password, - code + const response = await loginProxy({ + email, + password, + code + }); + + if (response.error) { + setError(response.message); + return; + } + + const data = response.data; + + // Handle case where data is null (e.g., already logged in) + if (!data) { + if (onLogin) { + await onLogin(); } - ); + return; + } - const data = res.data.data; - - if (data?.useSecurityKey) { + if (data.useSecurityKey) { await initiateSecurityKeyAuth(); return; } - if (data?.codeRequested) { + if (data.codeRequested) { setMfaRequested(true); setLoading(false); mfaForm.reset(); return; } - if (data?.emailVerificationRequired) { + if (data.emailVerificationRequired) { + if (!isExpectedHost) { + setError( + t("emailVerificationRequired", { + dashboardUrl: env.app.dashboardUrl + }) + ); + return; + } if (redirect) { router.push(`/auth/verify-email?redirect=${redirect}`); } else { @@ -240,7 +258,15 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { return; } - if (data?.twoFactorSetupRequired) { + if (data.twoFactorSetupRequired) { + if (!isExpectedHost) { + setError( + t("twoFactorSetupRequired", { + dashboardUrl: env.app.dashboardUrl + }) + ); + return; + } const setupUrl = `/auth/2fa/setup?email=${encodeURIComponent(email)}${redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""}`; router.push(setupUrl); return; @@ -250,50 +276,40 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { await onLogin(); } } catch (e: any) { - if (e.isAxiosError) { - const errorMessage = formatAxiosError( - e, - t("loginError", { - defaultValue: "Failed to log in" - }) - ); - setError(errorMessage); - return; - } else { - console.error(e); - setError( - e.message || - t("loginError", { - defaultValue: "Failed to log in" - }) - ); - return; - } + console.error(e); + setError( + t("loginError", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); } finally { setLoading(false); } } async function loginWithIdp(idpId: number) { + let redirectUrl: string | undefined; try { - const res = await api.post>( - `/auth/idp/${idpId}/oidc/generate-url`, - { - redirectUrl: redirect || "/" - } + const data = await generateOidcUrlProxy( + idpId, + redirect || "/", + orgId ); - - console.log(res); - - if (!res) { - setError(t("loginError")); + const url = data.data?.redirectUrl; + if (data.error) { + setError(data.message); return; } - - const data = res.data.data; - window.location.href = data.redirectUrl; - } catch (e) { - console.error(formatAxiosError(e)); + if (url) { + redirectUrl = url; + } + } catch (e: any) { + setError(e.message || t("loginError")); + console.error(e); + } + if (redirectUrl) { + redirectTo(redirectUrl); } } @@ -355,7 +371,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
{t("passwordForgot")} @@ -498,7 +514,8 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
{idps.map((idp) => { - const effectiveType = idp.variant || idp.name.toLowerCase(); + const effectiveType = + idp.variant || idp.name.toLowerCase(); return ( + + + + + ); +} + +export function PathRewriteModal({ + value, + onChange, + trigger, + disabled +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; + onChange: (config: { + rewritePath: string | null; + rewritePathType: string | null; + }) => void; + trigger: React.ReactNode; + disabled?: boolean; +}) { + const t = useTranslations(); + const [open, setOpen] = useState(false); + const [rewriteType, setRewriteType] = useState( + value?.rewritePathType || "prefix" + ); + const [rewritePath, setRewritePath] = useState(value?.rewritePath || ""); + + useEffect(() => { + if (open) { + setRewriteType(value?.rewritePathType || "prefix"); + setRewritePath(value?.rewritePath || ""); + } + }, [open, value]); + + const handleSave = () => { + onChange({ + rewritePathType: rewriteType as any, + rewritePath: rewritePath.trim() + }); + setOpen(false); + }; + + const handleClear = () => { + onChange({ rewritePathType: null, rewritePath: null }); + setOpen(false); + }; + + const getPlaceholder = () => { + switch (rewriteType) { + case "regex": + return t("pathRewriteRegexPlaceholder"); + case "stripPrefix": + return ""; + default: + return t("pathRewriteDefaultPlaceholder"); + } + }; + + const getHelpText = () => { + switch (rewriteType) { + case "prefix": + return t("pathRewritePrefixHelp"); + case "exact": + return t("pathRewriteExactHelp"); + case "regex": + return t("pathRewriteRegexHelp"); + case "stripPrefix": + return t("pathRewriteStripPrefixHelp"); + default: + return ""; + } + }; + + return ( + !disabled && setOpen(v)}> + {trigger} + + + {t("pathRewriteModalTitle")} + + {t("pathRewriteModalDescription")} + + +
+
+ + +
+
+ + setRewritePath(e.target.value)} + /> +

+ {getHelpText()} +

+
+
+ + {value?.rewritePath && ( + + )} + + +
+
+ ); +} + +export function PathMatchDisplay({ + value +}: { + value: { path: string | null; pathMatchType: string | null }; +}) { + const t = useTranslations(); + + if (!value?.path) return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: t("pathMatchPrefix"), + exact: t("pathMatchExact"), + regex: t("pathMatchRegex") + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.pathMatchType)} + + + {value.path} + + +
+ ); +} + +export function PathRewriteDisplay({ + value +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; +}) { + const t = useTranslations(); + + if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix") + return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: t("pathRewritePrefix"), + exact: t("pathRewriteExact"), + regex: t("pathRewriteRegex"), + stripPrefix: t("pathRewriteStrip") + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.rewritePathType)} + + + {value.rewritePath || ( + + ({t("pathRewriteStripLabel")}) + + )} + + +
+ ); +} diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 4760aeef..e87e7c98 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -8,6 +8,7 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import { useTranslations } from "next-intl"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type PermissionsSelectBoxProps = { root?: boolean; @@ -17,6 +18,7 @@ type PermissionsSelectBoxProps = { function getActionsCategories(root: boolean) { const t = useTranslations(); + const { env } = useEnvContext(); const actionsByCategory: Record> = { Organization: { @@ -34,12 +36,12 @@ function getActionsCategories(root: boolean) { }, Site: { - [t('actionCreateSite')]: "createSite", - [t('actionDeleteSite')]: "deleteSite", - [t('actionGetSite')]: "getSite", - [t('actionListSites')]: "listSites", - [t('actionUpdateSite')]: "updateSite", - [t('actionListSiteRoles')]: "listSiteRoles" + [t("actionCreateSite")]: "createSite", + [t("actionDeleteSite")]: "deleteSite", + [t("actionGetSite")]: "getSite", + [t("actionListSites")]: "listSites", + [t("actionUpdateSite")]: "updateSite", + [t("actionListSiteRoles")]: "listSiteRoles" }, Resource: { @@ -54,6 +56,7 @@ function getActionsCategories(root: boolean) { [t('actionListAllowedResourceRoles')]: "listResourceRoles", [t('actionSetResourcePassword')]: "setResourcePassword", [t('actionSetResourcePincode')]: "setResourcePincode", + [t('actionSetResourceHeaderAuth')]: "setResourceHeaderAuth", [t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist", [t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist", [t('actionCreateSiteResource')]: "createSiteResource", @@ -64,26 +67,26 @@ function getActionsCategories(root: boolean) { }, Target: { - [t('actionCreateTarget')]: "createTarget", - [t('actionDeleteTarget')]: "deleteTarget", - [t('actionGetTarget')]: "getTarget", - [t('actionListTargets')]: "listTargets", - [t('actionUpdateTarget')]: "updateTarget" + [t("actionCreateTarget")]: "createTarget", + [t("actionDeleteTarget")]: "deleteTarget", + [t("actionGetTarget")]: "getTarget", + [t("actionListTargets")]: "listTargets", + [t("actionUpdateTarget")]: "updateTarget" }, Role: { - [t('actionCreateRole')]: "createRole", - [t('actionDeleteRole')]: "deleteRole", - [t('actionGetRole')]: "getRole", - [t('actionListRole')]: "listRoles", - [t('actionUpdateRole')]: "updateRole", - [t('actionListAllowedRoleResources')]: "listRoleResources", - [t('actionAddUserRole')]: "addUserRole" + [t("actionCreateRole")]: "createRole", + [t("actionDeleteRole")]: "deleteRole", + [t("actionGetRole")]: "getRole", + [t("actionListRole")]: "listRoles", + [t("actionUpdateRole")]: "updateRole", + [t("actionListAllowedRoleResources")]: "listRoleResources", + [t("actionAddUserRole")]: "addUserRole" }, "Access Token": { - [t('actionGenerateAccessToken')]: "generateAccessToken", - [t('actionDeleteAccessToken')]: "deleteAcessToken", - [t('actionListAccessTokens')]: "listAccessTokens" + [t("actionGenerateAccessToken")]: "generateAccessToken", + [t("actionDeleteAccessToken")]: "deleteAcessToken", + [t("actionListAccessTokens")]: "listAccessTokens" }, "Resource Rule": { @@ -102,35 +105,45 @@ function getActionsCategories(root: boolean) { } }; + if (env.flags.enableClients) { + actionsByCategory["Clients"] = { + "Create Client": "createClient", + "Delete Client": "deleteClient", + "Update Client": "updateClient", + "List Clients": "listClients", + "Get Client": "getClient" + }; + } + if (root) { actionsByCategory["Organization"] = { - [t('actionListOrgs')]: "listOrgs", - [t('actionCheckOrgId')]: "checkOrgId", - [t('actionCreateOrg')]: "createOrg", - [t('actionDeleteOrg')]: "deleteOrg", - [t('actionListApiKeys')]: "listApiKeys", - [t('actionListApiKeyActions')]: "listApiKeyActions", - [t('actionSetApiKeyActions')]: "setApiKeyActions", - [t('actionCreateApiKey')]: "createApiKey", - [t('actionDeleteApiKey')]: "deleteApiKey", + [t("actionListOrgs")]: "listOrgs", + [t("actionCheckOrgId")]: "checkOrgId", + [t("actionCreateOrg")]: "createOrg", + [t("actionDeleteOrg")]: "deleteOrg", + [t("actionListApiKeys")]: "listApiKeys", + [t("actionListApiKeyActions")]: "listApiKeyActions", + [t("actionSetApiKeyActions")]: "setApiKeyActions", + [t("actionCreateApiKey")]: "createApiKey", + [t("actionDeleteApiKey")]: "deleteApiKey", ...actionsByCategory["Organization"] }; actionsByCategory["Identity Provider (IDP)"] = { - [t('actionCreateIdp')]: "createIdp", - [t('actionUpdateIdp')]: "updateIdp", - [t('actionDeleteIdp')]: "deleteIdp", - [t('actionListIdps')]: "listIdps", - [t('actionGetIdp')]: "getIdp", - [t('actionCreateIdpOrg')]: "createIdpOrg", - [t('actionDeleteIdpOrg')]: "deleteIdpOrg", - [t('actionListIdpOrgs')]: "listIdpOrgs", - [t('actionUpdateIdpOrg')]: "updateIdpOrg" + [t("actionCreateIdp")]: "createIdp", + [t("actionUpdateIdp")]: "updateIdp", + [t("actionDeleteIdp")]: "deleteIdp", + [t("actionListIdps")]: "listIdps", + [t("actionGetIdp")]: "getIdp", + [t("actionCreateIdpOrg")]: "createIdpOrg", + [t("actionDeleteIdpOrg")]: "deleteIdpOrg", + [t("actionListIdpOrgs")]: "listIdpOrgs", + [t("actionUpdateIdpOrg")]: "updateIdpOrg" }; actionsByCategory["User"] = { - [t('actionUpdateUser')]: "updateUser", - [t('actionGetUser')]: "getUser" + [t("actionUpdateUser")]: "updateUser", + [t("actionGetUser")]: "getUser" }; } @@ -189,7 +202,7 @@ export default function PermissionsSelectBox({ toggleAllPermissions(checked as boolean) @@ -208,7 +221,7 @@ export default function PermissionsSelectBox({ toggleAllInCategory( diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index d679b9b9..8372e756 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -31,19 +31,20 @@ import { } from "@app/components/ui/input-otp"; import { useRouter } from "next/navigation"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { formatAxiosError } from "@app/lib/api"; -import { AxiosResponse } from "axios"; import LoginForm, { LoginFormIDP } from "@app/components/LoginForm"; -import { - AuthWithPasswordResponse, - AuthWithWhitelistResponse -} from "@server/routers/resource"; import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; +import { + resourcePasswordProxy, + resourcePincodeProxy, + resourceWhitelistProxy, + resourceAccessProxy +} from "@app/actions/server"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import Link from "next/link"; import Image from "next/image"; +import BrandingLogo from "@app/components/BrandingLogo"; import { useTranslations } from "next-intl"; const pinSchema = z.object({ @@ -83,6 +84,7 @@ type ResourceAuthPortalProps = { }; redirect: string; idps?: LoginFormIDP[]; + orgId?: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { @@ -170,100 +172,129 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { return fullUrl.toString(); } - const onWhitelistSubmit = (values: any) => { + const onWhitelistSubmit = async (values: any) => { setLoadingLogin(true); - api.post>( - `/auth/resource/${props.resource.id}/whitelist`, - { email: values.email, otp: values.otp } - ) - .then((res) => { - setWhitelistError(null); + setWhitelistError(null); - if (res.data.data.otpSent) { - setOtpState("otp_sent"); - submitOtpForm.setValue("email", values.email); - toast({ - title: t("otpEmailSent"), - description: t("otpEmailSentDescription") - }); - return; - } + try { + const response = await resourceWhitelistProxy(props.resource.id, { + email: values.email, + otp: values.otp + }); - const session = res.data.data.session; - if (session) { - window.location.href = appendRequestToken( - props.redirect, - session - ); - } - }) - .catch((e) => { - console.error(e); - setWhitelistError( - formatAxiosError(e, t("otpEmailErrorAuthenticate")) - ); - }) - .then(() => setLoadingLogin(false)); - }; - - const onPinSubmit = (values: z.infer) => { - setLoadingLogin(true); - api.post>( - `/auth/resource/${props.resource.id}/pincode`, - { pincode: values.pin } - ) - .then((res) => { - setPincodeError(null); - const session = res.data.data.session; - if (session) { - window.location.href = appendRequestToken( - props.redirect, - session - ); - } - }) - .catch((e) => { - console.error(e); - setPincodeError( - formatAxiosError(e, t("pincodeErrorAuthenticate")) - ); - }) - .then(() => setLoadingLogin(false)); - }; - - const onPasswordSubmit = (values: z.infer) => { - setLoadingLogin(true); - - api.post>( - `/auth/resource/${props.resource.id}/password`, - { - password: values.password + if (response.error) { + setWhitelistError(response.message); + return; } - ) - .then((res) => { - setPasswordError(null); - const session = res.data.data.session; - if (session) { - window.location.href = appendRequestToken( - props.redirect, - session - ); - } - }) - .catch((e) => { - console.error(e); - setPasswordError( - formatAxiosError(e, t("passwordErrorAuthenticate")) + + const data = response.data!; + if (data.otpSent) { + setOtpState("otp_sent"); + submitOtpForm.setValue("email", values.email); + toast({ + title: t("otpEmailSent"), + description: t("otpEmailSentDescription") + }); + return; + } + + const session = data.session; + if (session) { + window.location.href = appendRequestToken( + props.redirect, + session ); - }) - .finally(() => setLoadingLogin(false)); + } + } catch (e: any) { + console.error(e); + setWhitelistError( + t("otpEmailErrorAuthenticate", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } finally { + setLoadingLogin(false); + } + }; + + const onPinSubmit = async (values: z.infer) => { + setLoadingLogin(true); + setPincodeError(null); + + try { + const response = await resourcePincodeProxy(props.resource.id, { + pincode: values.pin + }); + + if (response.error) { + setPincodeError(response.message); + return; + } + + const session = response.data!.session; + if (session) { + window.location.href = appendRequestToken( + props.redirect, + session + ); + } + } catch (e: any) { + console.error(e); + setPincodeError( + t("pincodeErrorAuthenticate", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } finally { + setLoadingLogin(false); + } + }; + + const onPasswordSubmit = async (values: z.infer) => { + setLoadingLogin(true); + setPasswordError(null); + + try { + const response = await resourcePasswordProxy(props.resource.id, { + password: values.password + }); + + if (response.error) { + setPasswordError(response.message); + return; + } + + const session = response.data!.session; + if (session) { + window.location.href = appendRequestToken( + props.redirect, + session + ); + } + } catch (e: any) { + console.error(e); + setPasswordError( + t("passwordErrorAuthenticate", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } finally { + setLoadingLogin(false); + } }; async function handleSSOAuth() { let isAllowed = false; try { - await api.get(`/resource/${props.resource.id}`); - isAllowed = true; + const response = await resourceAccessProxy(props.resource.id); + if (response.error) { + setAccessDenied(true); + } else { + isAllowed = true; + } } catch (e) { setAccessDenied(true); } @@ -275,34 +306,55 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } function getTitle() { + if (env.branding.resourceAuthPage?.titleText) { + return env.branding.resourceAuthPage.titleText; + } return t("authenticationRequired"); } function getSubtitle(resourceName: string) { + if (env.branding.resourceAuthPage?.subtitleText) { + return env.branding.resourceAuthPage.subtitleText + .split("{{resourceName}}") + .join(resourceName); + } return numMethods > 1 - ? t("authenticationMethodChoose", { name: props.resource.name }) - : t("authenticationRequest", { name: props.resource.name }); + ? t("authenticationMethodChoose", { name: resourceName }) + : t("authenticationRequest", { name: resourceName }); } + const logoWidth = env.branding.logo?.authPage?.width || 100100; + const logoHeight = env.branding.logo?.authPage?.height || 100; + return (
{!accessDenied ? (
-
- - {t("poweredBy")}{" "} - - Pangolin - - -
+ {!env.branding.resourceAuthPage?.hidePoweredBy && ( +
+ + {t("poweredBy")}{" "} + + {env.branding.appName || "Pangolin"} + + +
+ )} + {env.branding?.resourceAuthPage?.showLogo && ( +
+ +
+ )} {getTitle()} {getSubtitle(props.resource.name)} @@ -509,6 +561,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { await handleSSOAuth() } diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index 8da95ec0..50bb9af2 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -11,25 +11,27 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import { toUnicode } from 'punycode'; +import CertificateStatus from "@app/components/private/CertificateStatus"; +import { toUnicode } from "punycode"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type ResourceInfoBoxType = {}; -export default function ResourceInfoBox({ }: ResourceInfoBoxType) { +export default function ResourceInfoBox({}: ResourceInfoBoxType) { const { resource, authInfo } = useResourceContext(); + const { env } = useEnvContext(); const t = useTranslations(); - const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`; - - return ( - + {/* 4 cols because of the certs */} + {resource.http ? ( <> @@ -38,9 +40,10 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) { {authInfo.password || - authInfo.pincode || - authInfo.sso || - authInfo.whitelist ? ( + authInfo.pincode || + authInfo.sso || + authInfo.whitelist || + authInfo.headerAuth ? (
{t("protected")} @@ -118,6 +121,37 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) { )} */} )} + {/* */} + {/* {t('visibility')} */} + {/* */} + {/* */} + {/* {resource.enabled ? t('enabled') : t('disabled')} */} + {/* */} + {/* */} + {/* */} + {/* Certificate Status Column */} + {resource.http && + resource.domainId && + resource.fullDomain && + env.flags.usePangolinDns && ( + + + {t("certificateStatus", { + defaultValue: "Certificate" + })} + + + + + + )} {t("visibility")} diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index cb7983c7..ad8b4fab 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -24,7 +24,8 @@ import { MoreHorizontal, ArrowUpRight, ShieldOff, - ShieldCheck + ShieldCheck, + RefreshCw } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -179,9 +180,27 @@ export default function ResourcesTable({ const [internalColumnFilters, setInternalColumnFilters] = useState([]); const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); const currentView = searchParams.get("view") || defaultView; + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + useEffect(() => { const fetchSites = async () => { try { @@ -371,7 +390,7 @@ export default function ResourcesTable({
{!resourceRow.http ? ( ) : !resourceRow.domainId ? ( @@ -547,7 +566,7 @@ export default function ResourcesTable({ const resourceRow = row.original; return ( ); @@ -753,7 +772,21 @@ export default function ResourcesTable({ )}
- {getActionButton()} +
+ +
+
+ {getActionButton()} +
diff --git a/src/components/RolesDataTable.tsx b/src/components/RolesDataTable.tsx index e88f9a2f..8043fc23 100644 --- a/src/components/RolesDataTable.tsx +++ b/src/components/RolesDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createRole?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function RolesDataTable({ columns, data, - createRole + createRole, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function RolesDataTable({ searchPlaceholder={t('accessRolesSearch')} searchColumn="name" onAdd={createRole} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessRolesAdd')} /> ); diff --git a/src/components/RolesTable.tsx b/src/components/RolesTable.tsx index e92e71b6..292384a8 100644 --- a/src/components/RolesTable.tsx +++ b/src/components/RolesTable.tsx @@ -20,6 +20,7 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; export type RoleRow = Role; @@ -30,6 +31,7 @@ type RolesTableProps = { export default function UsersTable({ roles: r }: RolesTableProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const router = useRouter(); const [roles, setRoles] = useState(r); @@ -40,6 +42,24 @@ export default function UsersTable({ roles: r }: RolesTableProps) { const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -116,6 +136,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) { createRole={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/SetResourceHeaderAuthForm.tsx b/src/components/SetResourceHeaderAuthForm.tsx new file mode 100644 index 00000000..b1a75543 --- /dev/null +++ b/src/components/SetResourceHeaderAuthForm.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { formatAxiosError } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { Resource } from "@server/db"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; + +const setHeaderAuthFormSchema = z.object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100) +}); + +type SetHeaderAuthFormValues = z.infer; + +const defaultValues: Partial = { + user: "", + password: "" +}; + +type SetHeaderAuthFormProps = { + open: boolean; + setOpen: (open: boolean) => void; + resourceId: number; + onSetHeaderAuth?: () => void; +}; + +export default function SetResourceHeaderAuthForm({ + open, + setOpen, + resourceId, + onSetHeaderAuth +}: SetHeaderAuthFormProps) { + const api = createApiClient(useEnvContext()); + const t = useTranslations(); + + const [loading, setLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(setHeaderAuthFormSchema), + defaultValues + }); + + useEffect(() => { + if (!open) { + return; + } + + form.reset(); + }, [open]); + + async function onSubmit(data: SetHeaderAuthFormValues) { + setLoading(true); + + api.post>(`/resource/${resourceId}/header-auth`, { + user: data.user, + password: data.password + }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorHeaderAuthSetup'), + description: formatAxiosError( + e, + t('resourceErrorHeaderAuthSetupDescription') + ) + }); + }) + .then(() => { + toast({ + title: t('resourceHeaderAuthSetup'), + description: t('resourceHeaderAuthSetupDescription') + }); + + if (onSetHeaderAuth) { + onSetHeaderAuth(); + } + }) + .finally(() => setLoading(false)); + } + + return ( + <> + { + setOpen(val); + setLoading(false); + form.reset(); + }} + > + + + {t('resourceHeaderAuthSetupTitle')} + + {t('resourceHeaderAuthSetupTitleDescription')} + + + +
+ + ( + + {t('user')} + + + + + + )} + /> + ( + + {t('password')} + + + + + + )} + /> + + +
+ + + + + + +
+
+ + ); +} diff --git a/src/components/ShareLinksDataTable.tsx b/src/components/ShareLinksDataTable.tsx index dd266bcf..f2753bcf 100644 --- a/src/components/ShareLinksDataTable.tsx +++ b/src/components/ShareLinksDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createShareLink?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ShareLinksDataTable({ columns, data, - createShareLink + createShareLink, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function ShareLinksDataTable({ searchPlaceholder={t('shareSearch')} searchColumn="name" onAdd={createShareLink} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('shareCreate')} /> ); diff --git a/src/components/ShareLinksTable.tsx b/src/components/ShareLinksTable.tsx index 2943311f..ba9169c1 100644 --- a/src/components/ShareLinksTable.tsx +++ b/src/components/ShareLinksTable.tsx @@ -61,6 +61,25 @@ export default function ShareLinksTable({ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [rows, setRows] = useState(shareLinks); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + function formatLink(link: string) { return link.substring(0, 20) + "..." + link.substring(link.length - 20); } @@ -292,6 +311,8 @@ export default function ShareLinksTable({ createShareLink={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index a18e2560..c675a1eb 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -12,11 +12,13 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { Badge } from "./ui/badge"; export type SidebarNavItem = { href: string; title: string; icon?: React.ReactNode; + isBeta?: boolean; }; export type SidebarNavSection = { @@ -97,6 +99,14 @@ export function SidebarNav({ {!isCollapsed && ( <> {t(item.title)} + {item.isBeta && ( + + {t("beta")} + + )} )} diff --git a/src/components/SignupForm.tsx b/src/components/SignupForm.tsx index adb7c83f..d155acb3 100644 --- a/src/components/SignupForm.tsx +++ b/src/components/SignupForm.tsx @@ -18,9 +18,7 @@ import { import { Card, CardContent, - CardDescription, CardHeader, - CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Progress } from "@/components/ui/progress"; @@ -31,11 +29,9 @@ import { AxiosResponse } from "axios"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import Image from "next/image"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; -import { build } from "@server/build"; import { Check, X } from "lucide-react"; import { cn } from "@app/lib/cn"; @@ -82,19 +78,7 @@ const formSchema = z .object({ email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, - confirmPassword: passwordSchema, - agreeToTerms: z.boolean().refine( - (val) => { - if (build === "saas") { - val === true; - } - return true; - }, - { - message: - "You must agree to the terms of service and privacy policy" - } - ) + confirmPassword: passwordSchema }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], @@ -108,12 +92,12 @@ export default function SignupForm({ emailParam }: SignupFormProps) { const router = useRouter(); - const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + const api = createApiClient({ env }); const t = useTranslations(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [termsAgreedAt, setTermsAgreedAt] = useState(null); const [passwordValue, setPasswordValue] = useState(""); const [confirmPasswordValue, setConfirmPasswordValue] = useState(""); @@ -122,14 +106,16 @@ export default function SignupForm({ defaultValues: { email: emailParam || "", password: "", - confirmPassword: "", - agreeToTerms: false + confirmPassword: "" }, mode: "onChange" // Enable real-time validation }); const passwordStrength = calculatePasswordStrength(passwordValue); - const doPasswordsMatch = passwordValue.length > 0 && confirmPasswordValue.length > 0 && passwordValue === confirmPasswordValue; + const doPasswordsMatch = + passwordValue.length > 0 && + confirmPasswordValue.length > 0 && + passwordValue === confirmPasswordValue; async function onSubmit(values: z.infer) { const { email, password } = values; @@ -140,8 +126,7 @@ export default function SignupForm({ email, password, inviteId, - inviteToken, - termsAcceptedTimestamp: termsAgreedAt + inviteToken }) .catch((e) => { console.error(e); @@ -176,23 +161,14 @@ export default function SignupForm({ return t("authCreateAccount"); } - const handleTermsChange = (checked: boolean) => { - if (checked) { - const isoNow = new Date().toISOString(); - console.log("Terms agreed at:", isoNow); - setTermsAgreedAt(isoNow); - form.setValue("agreeToTerms", true); - } else { - form.setValue("agreeToTerms", false); - setTermsAgreedAt(null); - } - }; + const logoWidth = env.branding.logo?.authPage?.width || 175; + const logoHeight = env.branding.logo?.authPage?.height || 58; return (
- +

{getSubtitle()}

@@ -211,8 +187,8 @@ export default function SignupForm({ {t("email")} - @@ -227,7 +203,8 @@ export default function SignupForm({
{t("password")} - {passwordStrength.strength === "strong" && ( + {passwordStrength.strength === + "strong" && ( )}
@@ -238,115 +215,193 @@ export default function SignupForm({ {...field} onChange={(e) => { field.onChange(e); - setPasswordValue(e.target.value); + setPasswordValue( + e.target.value + ); }} className={cn( - passwordStrength.strength === "strong" && "border-green-500 focus-visible:ring-green-500", - passwordStrength.strength === "medium" && "border-yellow-500 focus-visible:ring-yellow-500", - passwordStrength.strength === "weak" && passwordValue.length > 0 && "border-red-500 focus-visible:ring-red-500" + passwordStrength.strength === + "strong" && + "border-green-500 focus-visible:ring-green-500", + passwordStrength.strength === + "medium" && + "border-yellow-500 focus-visible:ring-yellow-500", + passwordStrength.strength === + "weak" && + passwordValue.length > + 0 && + "border-red-500 focus-visible:ring-red-500" )} autoComplete="new-password" />
- + {passwordValue.length > 0 && (
{/* Password Strength Meter */}
- {t("passwordStrength")} - - {t(`passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}`)} + + {t("passwordStrength")} + + + {t( + `passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}` + )}
-
- + {/* Requirements Checklist */}
-
{t("passwordRequirements")}
+
+ {t("passwordRequirements")} +
- {passwordStrength.requirements.length ? ( + {passwordStrength + .requirements + .length ? ( ) : ( )} - - {t("passwordRequirementLengthText")} + + {t( + "passwordRequirementLengthText" + )}
- {passwordStrength.requirements.uppercase ? ( + {passwordStrength + .requirements + .uppercase ? ( ) : ( )} - - {t("passwordRequirementUppercaseText")} + + {t( + "passwordRequirementUppercaseText" + )}
- {passwordStrength.requirements.lowercase ? ( + {passwordStrength + .requirements + .lowercase ? ( ) : ( )} - - {t("passwordRequirementLowercaseText")} + + {t( + "passwordRequirementLowercaseText" + )}
- {passwordStrength.requirements.number ? ( + {passwordStrength + .requirements + .number ? ( ) : ( )} - - {t("passwordRequirementNumberText")} + + {t( + "passwordRequirementNumberText" + )}
- {passwordStrength.requirements.special ? ( + {passwordStrength + .requirements + .special ? ( ) : ( )} - - {t("passwordRequirementSpecialText")} + + {t( + "passwordRequirementSpecialText" + )}
)} - + {/* Only show FormMessage when not showing our custom requirements */} - {passwordValue.length === 0 && } + {passwordValue.length === 0 && ( + + )} )} /> @@ -356,7 +411,9 @@ export default function SignupForm({ render={({ field }) => (
- {t('confirmPassword')} + + {t("confirmPassword")} + {doPasswordsMatch && ( )} @@ -368,74 +425,35 @@ export default function SignupForm({ {...field} onChange={(e) => { field.onChange(e); - setConfirmPasswordValue(e.target.value); + setConfirmPasswordValue( + e.target.value + ); }} className={cn( - doPasswordsMatch && "border-green-500 focus-visible:ring-green-500", - confirmPasswordValue.length > 0 && !doPasswordsMatch && "border-red-500 focus-visible:ring-red-500" + doPasswordsMatch && + "border-green-500 focus-visible:ring-green-500", + confirmPasswordValue.length > + 0 && + !doPasswordsMatch && + "border-red-500 focus-visible:ring-red-500" )} autoComplete="new-password" />
- {confirmPasswordValue.length > 0 && !doPasswordsMatch && ( -

- {t("passwordsDoNotMatch")} -

- )} + {confirmPasswordValue.length > 0 && + !doPasswordsMatch && ( +

+ {t("passwordsDoNotMatch")} +

+ )} {/* Only show FormMessage when field is empty */} - {confirmPasswordValue.length === 0 && } + {confirmPasswordValue.length === 0 && ( + + )}
)} /> - {build === "saas" && ( - ( - - - { - field.onChange(checked); - handleTermsChange( - checked as boolean - ); - }} - /> - -
- - {t("signUpTerms.IAgreeToThe")} - - {t( - "signUpTerms.termsOfService" - )} - - {t("signUpTerms.and")} - - {t( - "signUpTerms.privacyPolicy" - )} - - - -
-
- )} - /> - )} {error && ( @@ -451,4 +469,4 @@ export default function SignupForm({
); -} \ No newline at end of file +} diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index f9ac8c0d..851514ea 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -42,6 +42,8 @@ export type SiteRow = { newtUpdateAvailable?: boolean; online: boolean; address?: string; + exitNodeName?: string; + exitNodeEndpoint?: string; }; type SitesTableProps = { @@ -280,9 +282,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { } } }, - ...(env.flags.enableClients ? [{ - accessorKey: "address", - header: ({ column }: { column: Column }) => { + { + accessorKey: "exitNode", + header: ({ column }) => { return ( ); + }, + cell: ({ row }) => { + const originalRow = row.original; + return originalRow.exitNodeName ? ( +
+ {originalRow.exitNodeName} +
+ ) : ( + "-" + ); } - }] : []), + }, + ...(env.flags.enableClients + ? [ + { + accessorKey: "address", + header: ({ + column + }: { + column: Column; + }) => { + return ( + + ); + }, + cell: ({ row }: { row: any }) => { + const originalRow = row.original; + return originalRow.address ? ( +
+ {originalRow.address} +
+ ) : ( + "-" + ); + } + } + ] + : []), { id: "actions", cell: ({ row }) => { diff --git a/src/components/StrategySelect.tsx b/src/components/StrategySelect.tsx index 374d1bae..1b922cd1 100644 --- a/src/components/StrategySelect.tsx +++ b/src/components/StrategySelect.tsx @@ -5,9 +5,9 @@ import { RadioGroup, RadioGroupItem } from "./ui/radio-group"; import { useState, ReactNode } from "react"; export interface StrategyOption { -id: TValue; + id: TValue; title: string; - description: string; + description: string | ReactNode; disabled?: boolean; icon?: ReactNode; } @@ -59,16 +59,16 @@ export function StrategySelect({ disabled={option.disabled} className="absolute left-4 top-5 h-4 w-4 border-primary text-primary" /> -
+
{option.icon && ( -
+
{option.icon}
)}
{option.title}
- {option.description} + {typeof option.description === 'string' ? option.description : option.description}
diff --git a/src/components/TwoFactorSetupForm.tsx b/src/components/TwoFactorSetupForm.tsx index b36354d0..0c0b657e 100644 --- a/src/components/TwoFactorSetupForm.tsx +++ b/src/components/TwoFactorSetupForm.tsx @@ -230,7 +230,9 @@ const TwoFactorSetupForm = forwardRef<

{t("otpSetupScanQr")}

+
+
diff --git a/src/components/UsersDataTable.tsx b/src/components/UsersDataTable.tsx index 1999b620..db12b697 100644 --- a/src/components/UsersDataTable.tsx +++ b/src/components/UsersDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; inviteUser?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, data, - inviteUser + inviteUser, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function UsersDataTable({ searchPlaceholder={t('accessUsersSearch')} searchColumn="email" onAdd={inviteUser} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessUserCreate')} /> ); diff --git a/src/components/UsersTable.tsx b/src/components/UsersTable.tsx index 57fe5c91..d53047d1 100644 --- a/src/components/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -51,6 +51,24 @@ export default function UsersTable({ users: u }: UsersTableProps) { const { user, updateUser } = useUserContext(); const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -290,6 +308,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { `/${org?.org.orgId}/settings/access/users/create` ); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx index bc87d1c1..a99af4c7 100644 --- a/src/components/ValidateOidcToken.tsx +++ b/src/components/ValidateOidcToken.tsx @@ -2,7 +2,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ValidateOidcUrlCallbackResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -16,6 +15,7 @@ import { import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; import { useTranslations } from "next-intl"; +import { validateOidcUrlCallbackProxy } from "@app/actions/server"; type ValidateOidcTokenParams = { orgId: string; @@ -24,6 +24,7 @@ type ValidateOidcTokenParams = { expectedState: string | undefined; stateCookie: string | undefined; idp: { name: string }; + loginPageId?: number; }; export default function ValidateOidcToken(props: ValidateOidcTokenParams) { @@ -40,39 +41,55 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { async function validate() { setLoading(true); - console.log(t('idpOidcTokenValidating'), { + console.log(t("idpOidcTokenValidating"), { code: props.code, expectedState: props.expectedState, stateCookie: props.stateCookie }); try { - const res = await api.post< - AxiosResponse - >(`/auth/idp/${props.idpId}/oidc/validate-callback`, { - code: props.code, - state: props.expectedState, - storedState: props.stateCookie - }); + const response = await validateOidcUrlCallbackProxy( + props.idpId, + props.code || "", + props.expectedState || "", + props.stateCookie || "", + props.loginPageId + ); - console.log(t('idpOidcTokenResponse'), res.data); + if (response.error) { + setError(response.message); + setLoading(false); + return; + } - const redirectUrl = res.data.data.redirectUrl; + const data = response.data; + if (!data) { + setError("Unable to validate OIDC token"); + setLoading(false); + return; + } + + const redirectUrl = data.redirectUrl; if (!redirectUrl) { - router.push("/"); + router.push(env.app.dashboardUrl); } setLoading(false); await new Promise((resolve) => setTimeout(resolve, 100)); if (redirectUrl.startsWith("http")) { - window.location.href = res.data.data.redirectUrl; // this is validated by the parent using this component + window.location.href = data.redirectUrl; // this is validated by the parent using this component } else { - router.push(res.data.data.redirectUrl); + router.push(data.redirectUrl); } - } catch (e) { - setError(formatAxiosError(e, t('idpErrorOidcTokenValidating'))); + } catch (e: any) { + console.error(e); + setError( + t("idpErrorOidcTokenValidating", { + defaultValue: "An unexpected error occurred. Please try again." + }) + ); } finally { setLoading(false); } @@ -85,20 +102,24 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
- {t('idpConnectingTo', {name: props.idp.name})} - {t('idpConnectingToDescription')} + + {t("idpConnectingTo", { name: props.idp.name })} + + + {t("idpConnectingToDescription")} + {loading && (
- {t('idpConnectingToProcess')} + {t("idpConnectingToProcess")}
)} {!loading && !error && (
- {t('idpConnectingToFinished')} + {t("idpConnectingToFinished")}
)} {error && ( @@ -106,7 +127,9 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { - {t('idpErrorConnectingTo', {name: props.idp.name})} + {t("idpErrorConnectingTo", { + name: props.idp.name + })} {error} diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx new file mode 100644 index 00000000..6754f009 --- /dev/null +++ b/src/components/private/AuthPageSettings.tsx @@ -0,0 +1,534 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { toast } from "@app/hooks/useToast"; +import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { formatAxiosError } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; +import { + SettingsSection, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionForm +} from "@app/components/Settings"; +import { useTranslations } from "next-intl"; +import { GetLoginPageResponse } from "@server/routers/loginPage/types"; +import { ListDomainsResponse } from "@server/routers/domain"; +import { DomainRow } from "@app/components/DomainsTable"; +import { toUnicode } from "punycode"; +import { Globe, Trash2 } from "lucide-react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import DomainPicker from "@app/components/DomainPicker"; +import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; + +// Auth page form schema +const AuthPageFormSchema = z.object({ + authPageDomainId: z.string().optional(), + authPageSubdomain: z.string().optional() +}); + +type AuthPageFormValues = z.infer; + +interface AuthPageSettingsProps { + onSaveSuccess?: () => void; + onSaveError?: (error: any) => void; +} + +export interface AuthPageSettingsRef { + saveAuthSettings: () => Promise; + hasUnsavedChanges: () => boolean; +} + +const AuthPageSettings = forwardRef( + ({ onSaveSuccess, onSaveError }, ref) => { + const { org } = useOrgContext(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const t = useTranslations(); + const { env } = useEnvContext(); + + // Auth page domain state + const [loginPage, setLoginPage] = useState( + null + ); + const [loginPageExists, setLoginPageExists] = useState(false); + const [editDomainOpen, setEditDomainOpen] = useState(false); + const [baseDomains, setBaseDomains] = useState([]); + const [selectedDomain, setSelectedDomain] = useState<{ + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + } | null>(null); + const [loadingLoginPage, setLoadingLoginPage] = useState(true); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [loadingSave, setLoadingSave] = useState(false); + + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + authPageDomainId: loginPage?.domainId || "", + authPageSubdomain: loginPage?.subdomain || "" + }, + mode: "onChange" + }); + + // Expose save function to parent component + useImperativeHandle( + ref, + () => ({ + saveAuthSettings: async () => { + await form.handleSubmit(onSubmit)(); + }, + hasUnsavedChanges: () => hasUnsavedChanges + }), + [form, hasUnsavedChanges] + ); + + // Fetch login page and domains data + useEffect(() => { + const fetchLoginPage = async () => { + try { + const res = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + if (res.status === 200) { + setLoginPage(res.data.data); + setLoginPageExists(true); + // Update form with login page data + form.setValue( + "authPageDomainId", + res.data.data.domainId || "" + ); + form.setValue( + "authPageSubdomain", + res.data.data.subdomain || "" + ); + } + } catch (err) { + // Login page doesn't exist yet, that's okay + setLoginPage(null); + setLoginPageExists(false); + } finally { + setLoadingLoginPage(false); + } + }; + + const fetchDomains = async () => { + try { + const res = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/domains/`); + if (res.status === 200) { + const rawDomains = res.data.data.domains as DomainRow[]; + const domains = rawDomains.map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain) + })); + setBaseDomains(domains); + } + } catch (err) { + console.error("Failed to fetch domains:", err); + } + }; + + if (org?.org.orgId) { + fetchLoginPage(); + fetchDomains(); + } + }, []); + + // Handle domain selection from modal + function handleDomainSelection(domain: { + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + }) { + form.setValue("authPageDomainId", domain.domainId); + form.setValue("authPageSubdomain", domain.subdomain || ""); + setEditDomainOpen(false); + + // Update loginPage state to show the selected domain immediately + const sanitizedSubdomain = domain.subdomain + ? finalizeSubdomainSanitize(domain.subdomain) + : ""; + + const sanitizedFullDomain = sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + + // Only update loginPage state if a login page already exists + if (loginPageExists && loginPage) { + setLoginPage({ + ...loginPage, + domainId: domain.domainId, + subdomain: sanitizedSubdomain, + fullDomain: sanitizedFullDomain + }); + } + + setHasUnsavedChanges(true); + } + + // Clear auth page domain + function clearAuthPageDomain() { + form.setValue("authPageDomainId", ""); + form.setValue("authPageSubdomain", ""); + setLoginPage(null); + setHasUnsavedChanges(true); + } + + async function onSubmit(data: AuthPageFormValues) { + setLoadingSave(true); + + try { + // Handle auth page domain + if (data.authPageDomainId) { + /*if ( + build === "enterprise" || + (build === "saas" && subscription?.subscribed) + ) { + const sanitizedSubdomain = data.authPageSubdomain + ? finalizeSubdomainSanitize(data.authPageSubdomain) + : ""; + + if (loginPageExists) { + // Login page exists on server - need to update it + // First, we need to get the loginPageId from the server since loginPage might be null locally + let loginPageId: number; + + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; + } else { + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; + } + + // Update existing auth page domain + const updateRes = await api.post( + `/org/${org?.org.orgId}/login-page/${loginPageId}`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null + } + ); + + if (updateRes.status === 201) { + setLoginPage(updateRes.data.data); + setLoginPageExists(true); + } + } else { + // No login page exists on server - create new one + const createRes = await api.put( + `/org/${org?.org.orgId}/login-page`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null + } + ); + + if (createRes.status === 201) { + setLoginPage(createRes.data.data); + setLoginPageExists(true); + } + } + }*/ + } else if (loginPageExists) { + // Delete existing auth page domain if no domain selected + let loginPageId: number; + + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; + } else { + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; + } + + await api.delete( + `/org/${org?.org.orgId}/login-page/${loginPageId}` + ); + setLoginPage(null); + setLoginPageExists(false); + } + + setHasUnsavedChanges(false); + router.refresh(); + onSaveSuccess?.(); + } catch (e) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + e, + t("authPageErrorUpdateMessage") + ) + }); + onSaveError?.(e); + } finally { + setLoadingSave(false); + } + } + + return ( + <> + + + + {t("authPage")} + + + {t("authPageDescription")} + + + + + {loadingLoginPage ? ( +
+
+ {t("loading")} +
+
+ ) : ( +
+ +
+ +
+ + + {loginPage && + !loginPage.domainId ? ( + + ) : loginPage?.fullDomain ? ( + + {`${window.location.protocol}//${loginPage.fullDomain}`} + + ) : form.watch( + "authPageDomainId" + ) ? ( + // Show selected domain from form state when no loginPage exists yet + (() => { + const selectedDomainId = + form.watch( + "authPageDomainId" + ); + const selectedSubdomain = + form.watch( + "authPageSubdomain" + ); + const domain = + baseDomains.find( + (d) => + d.domainId === + selectedDomainId + ); + if (domain) { + const sanitizedSubdomain = + selectedSubdomain + ? finalizeSubdomainSanitize( + selectedSubdomain + ) + : ""; + const fullDomain = + sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + return fullDomain; + } + return t( + "noDomainSet" + ); + })() + ) : ( + t("noDomainSet") + )} + +
+ + {form.watch( + "authPageDomainId" + ) && ( + + )} +
+
+ + {!form.watch( + "authPageDomainId" + ) && ( +
+ {t( + "addDomainToEnableCustomAuthPages" + )} +
+ )} + + {/*env.flags + .usePangolinDns && + (build === "enterprise" || + (build === "saas" && + subscription?.subscribed)) && + loginPage?.domainId && + loginPage?.fullDomain && + !hasUnsavedChanges && ( + + )*/} +
+
+ + )} +
+
+
+ + {/* Domain Picker Modal */} + setEditDomainOpen(setOpen)} + > + + + + {loginPage + ? t("editAuthPageDomain") + : t("setAuthPageDomain")} + + + {t("selectDomainForOrgAuthPage")} + + + + { + const selected = { + domainId: res.domainId, + subdomain: res.subdomain, + fullDomain: res.fullDomain, + baseDomain: res.baseDomain + }; + setSelectedDomain(selected); + }} + /> + + + + + + + + + + + ); + } +); + +AuthPageSettings.displayName = "AuthPageSettings"; + +export default AuthPageSettings; diff --git a/src/components/private/AutoProvisionConfigWidget.tsx b/src/components/private/AutoProvisionConfigWidget.tsx new file mode 100644 index 00000000..159ba01e --- /dev/null +++ b/src/components/private/AutoProvisionConfigWidget.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage +} from "@app/components/ui/form"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Input } from "@app/components/ui/input"; +import { useTranslations } from "next-intl"; +import { Control, FieldValues, Path } from "react-hook-form"; + +type Role = { + roleId: number; + name: string; +}; + +type AutoProvisionConfigWidgetProps = { + control: Control; + autoProvision: boolean; + onAutoProvisionChange: (checked: boolean) => void; + roleMappingMode: "role" | "expression"; + onRoleMappingModeChange: (mode: "role" | "expression") => void; + roles: Role[]; + roleIdFieldName: Path; + roleMappingFieldName: Path; +}; + +export default function AutoProvisionConfigWidget({ + control, + autoProvision, + onAutoProvisionChange, + roleMappingMode, + onRoleMappingModeChange, + roles, + roleIdFieldName, + roleMappingFieldName +}: AutoProvisionConfigWidgetProps) { + const t = useTranslations(); + + return ( +
+
+ + + {t("idpAutoProvisionUsersDescription")} + +
+ + {autoProvision && ( +
+
+ + {t("roleMapping")} + + + {t("roleMappingDescription")} + + + +
+ + +
+
+ + +
+
+
+ + {roleMappingMode === "role" ? ( + ( + + + + {t("selectRoleDescription")} + + + + )} + /> + ) : ( + ( + + + + + + {t("roleMappingExpressionDescription")} + + + + )} + /> + )} +
+ )} +
+ ); +} diff --git a/src/components/private/CertificateStatus.tsx b/src/components/private/CertificateStatus.tsx new file mode 100644 index 00000000..f1446806 --- /dev/null +++ b/src/components/private/CertificateStatus.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { RotateCw } from "lucide-react"; +import { useCertificate } from "@app/hooks/useCertificate"; +import { useTranslations } from "next-intl"; + +type CertificateStatusProps = { + orgId: string; + domainId: string; + fullDomain: string; + autoFetch?: boolean; + showLabel?: boolean; + className?: string; + onRefresh?: () => void; + polling?: boolean; + pollingInterval?: number; +}; + +export default function CertificateStatus({ + orgId, + domainId, + fullDomain, + autoFetch = true, + showLabel = true, + className = "", + onRefresh, + polling = false, + pollingInterval = 5000 +}: CertificateStatusProps) { + const t = useTranslations(); + const { cert, certLoading, certError, refreshing, refreshCert } = useCertificate({ + orgId, + domainId, + fullDomain, + autoFetch, + polling, + pollingInterval + }); + + const handleRefresh = async () => { + await refreshCert(); + onRefresh?.(); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "valid": + return "text-green-500"; + case "pending": + case "requested": + return "text-yellow-500"; + case "expired": + case "failed": + return "text-red-500"; + default: + return "text-muted-foreground"; + } + }; + + const shouldShowRefreshButton = (status: string, updatedAt: string) => { + return ( + status === "failed" || + status === "expired" || + (status === "requested" && + updatedAt && new Date(updatedAt).getTime() < Date.now() - 5 * 60 * 1000) + ); + }; + + if (certLoading) { + return ( +
+ {showLabel && ( + + {t("certificateStatus")}: + + )} + + {t("loading")} + +
+ ); + } + + if (certError) { + return ( +
+ {showLabel && ( + + {t("certificateStatus")}: + + )} + + {certError} + +
+ ); + } + + if (!cert) { + return ( +
+ {showLabel && ( + + {t("certificateStatus")}: + + )} + + {t("none", { defaultValue: "None" })} + +
+ ); + } + + return ( +
+ {showLabel && ( + + {t("certificateStatus")}: + + )} + + + {cert.status.charAt(0).toUpperCase() + cert.status.slice(1)} + {shouldShowRefreshButton(cert.status, cert.updatedAt) && ( + + )} + + +
+ ); +} diff --git a/src/components/private/IdpLoginButtons.tsx b/src/components/private/IdpLoginButtons.tsx new file mode 100644 index 00000000..c2ec1f5b --- /dev/null +++ b/src/components/private/IdpLoginButtons.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { useTranslations } from "next-intl"; +import Image from "next/image"; +import { + generateOidcUrlProxy, + type GenerateOidcUrlResponse +} from "@app/actions/server"; +import { + redirect as redirectTo, + useParams, + useSearchParams +} from "next/navigation"; +import { useRouter } from "next/navigation"; + +export type LoginFormIDP = { + idpId: number; + name: string; + variant?: string; +}; + +type IdpLoginButtonsProps = { + idps: LoginFormIDP[]; + redirect?: string; + orgId?: string; +}; + +export default function IdpLoginButtons({ + idps, + redirect, + orgId +}: IdpLoginButtonsProps) { + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const t = useTranslations(); + + const params = useSearchParams(); + const router = useRouter(); + + function goToApp() { + const url = window.location.href.split("?")[0]; + router.push(url); + } + + useEffect(() => { + if (params.get("gotoapp")) { + goToApp(); + } + }, []); + + async function loginWithIdp(idpId: number) { + setLoading(true); + setError(null); + + let redirectToUrl: string | undefined; + try { + const response = await generateOidcUrlProxy( + idpId, + redirect || "/auth/org?gotoapp=app", + orgId + ); + + if (response.error) { + setError(response.message); + setLoading(false); + return; + } + + const data = response.data; + console.log("Redirecting to:", data?.redirectUrl); + if (data?.redirectUrl) { + redirectToUrl = data.redirectUrl; + } + } catch (e: any) { + console.error(e); + setError( + t("loginError", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + setLoading(false); + } + + if (redirectToUrl) { + redirectTo(redirectToUrl); + } + } + + if (!idps || idps.length === 0) { + return null; + } + + return ( +
+ {error && ( + + {error} + + )} + +
+ {params.get("gotoapp") ? ( + <> + + + ) : ( + <> + {idps.map((idp) => { + const effectiveType = + idp.variant || idp.name.toLowerCase(); + + return ( + + ); + })} + + )} +
+
+ ); +} diff --git a/src/components/private/OrgIdpDataTable.tsx b/src/components/private/OrgIdpDataTable.tsx new file mode 100644 index 00000000..a7dc1850 --- /dev/null +++ b/src/components/private/OrgIdpDataTable.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; +import { useTranslations } from "next-intl"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onAdd?: () => void; +} + +export function IdpDataTable({ + columns, + data, + onAdd +}: DataTableProps) { + const t = useTranslations(); + + return ( + + ); +} diff --git a/src/components/private/OrgIdpTable.tsx b/src/components/private/OrgIdpTable.tsx new file mode 100644 index 00000000..436904a0 --- /dev/null +++ b/src/components/private/OrgIdpTable.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { IdpDataTable } from "@app/components/private/OrgIdpDataTable"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useRouter } from "next/navigation"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import IdpTypeBadge from "@app/components/IdpTypeBadge"; + +export type IdpRow = { + idpId: number; + name: string; + type: string; + variant?: string; +}; + +type Props = { + idps: IdpRow[]; + orgId: string; +}; + +export default function IdpTable({ idps, orgId }: Props) { + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedIdp, setSelectedIdp] = useState(null); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const t = useTranslations(); + + const deleteIdp = async (idpId: number) => { + try { + await api.delete(`/org/${orgId}/idp/${idpId}`); + toast({ + title: t("success"), + description: t("idpDeletedDescription") + }); + setIsDeleteModalOpen(false); + router.refresh(); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e), + variant: "destructive" + }); + } + }; + + + const columns: ColumnDef[] = [ + { + accessorKey: "idpId", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "type", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const type = row.original.type; + const variant = row.original.variant; + return ( + + ); + } + }, + { + id: "actions", + cell: ({ row }) => { + const siteRow = row.original; + return ( +
+ + + + + + + + {t("viewSettings")} + + + { + setSelectedIdp(siteRow); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + + + + +
+ ); + } + } + ]; + + return ( + <> + {selectedIdp && ( + { + setIsDeleteModalOpen(val); + setSelectedIdp(null); + }} + dialog={ +
+

+ {t("idpQuestionRemove", { + name: selectedIdp.name + })} +

+

+ {t("idpMessageRemove")} +

+

{t("idpMessageConfirm")}

+
+ } + buttonText={t("idpConfirmDelete")} + onConfirm={async () => deleteIdp(selectedIdp.idpId)} + string={selectedIdp.name} + title={t("idpDelete")} + /> + )} + + router.push(`/${orgId}/settings/idp/create`)} + /> + + ); +} diff --git a/src/components/private/RegionSelector.tsx b/src/components/private/RegionSelector.tsx new file mode 100644 index 00000000..f3928345 --- /dev/null +++ b/src/components/private/RegionSelector.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { useTranslations } from "next-intl"; + +type Region = { + value: string; + label: string; + flag: string; +}; + +const regions: Region[] = [ + { + value: "us", + label: "North America", + flag: "" + }, + { + value: "eu", + label: "Europe", + flag: "" + } +]; + +export default function RegionSelector() { + const [selectedRegion, setSelectedRegion] = useState("us"); + const t = useTranslations(); + + const handleRegionChange = (value: string) => { + setSelectedRegion(value); + const region = regions.find((r) => r.value === value); + if (region) { + console.log(`Selected region: ${region.label}`); + } + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/private/SplashImage.tsx b/src/components/private/SplashImage.tsx new file mode 100644 index 00000000..c6ddc466 --- /dev/null +++ b/src/components/private/SplashImage.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePathname } from "next/navigation"; +import Image from "next/image"; + +type SplashImageProps = { + children: React.ReactNode; +}; + +export default function SplashImage({ children }: SplashImageProps) { + const pathname = usePathname(); + const { env } = useEnvContext(); + + function showBackgroundImage() { + if (!env.branding.background_image_path) { + return false; + } + const pathsPrefixes = ["/auth/login", "/auth/signup", "/auth/resource"]; + for (const prefix of pathsPrefixes) { + if (pathname.startsWith(prefix)) { + return true; + } + } + return false; + } + + return ( + <> + {showBackgroundImage() && ( + Background + )} + + {children} + + ); +} diff --git a/src/components/private/ValidateSessionTransferToken.tsx b/src/components/private/ValidateSessionTransferToken.tsx new file mode 100644 index 00000000..fcb6a026 --- /dev/null +++ b/src/components/private/ValidateSessionTransferToken.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { redirect, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { TransferSessionResponse } from "@server/routers/auth/types"; + +type ValidateSessionTransferTokenParams = { + token: string; +}; + +export default function ValidateSessionTransferToken( + props: ValidateSessionTransferTokenParams +) { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const router = useRouter(); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const t = useTranslations(); + + useEffect(() => { + async function validate() { + setLoading(true); + + let doRedirect = false; + try { + const res = await api.post< + AxiosResponse + >(`/auth/transfer-session-token`, { + token: props.token + }); + + if (res && res.status === 200) { + doRedirect = true; + } + } catch (e) { + console.error(e); + setError(formatAxiosError(e, "Failed to validate token")); + } finally { + setLoading(false); + } + + if (doRedirect) { + redirect(env.app.dashboardUrl); + } + } + + validate(); + }, []); + + return ( +
+ {error && ( + + + + {error} + + + )} +
+ ); +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 2c30ee73..dff7777c 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -14,7 +14,9 @@ const alertVariants = cva( "border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive", success: "border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500", - info: "border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500" + info: "border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500", + warning: + "border-yellow-500/50 border bg-yellow-500/10 text-yellow-500 dark:border-yellow-400 [&>svg]:text-yellow-500" } }, defaultVariants: { diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index dcfc646d..83e84073 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = React.forwardRef( return (