diff --git a/.dockerignore b/.dockerignore index 8bb9e854..816d8ee3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,7 +26,3 @@ install/ bruno/ LICENSE CONTRIBUTING.md -dist -.git -migrations/ -config/ diff --git a/.github/DISCUSSION_TEMPLATE/feature-requests.yml b/.github/DISCUSSION_TEMPLATE/feature-requests.yml deleted file mode 100644 index 03b580ca..00000000 --- a/.github/DISCUSSION_TEMPLATE/feature-requests.yml +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 41dbe7bf..00000000 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index a3739c4d..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -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/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 196676e9..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - groups: - dev-patch-updates: - dependency-type: "development" - update-types: - - "patch" - dev-minor-updates: - dependency-type: "development" - update-types: - - "minor" - prod-patch-updates: - dependency-type: "production" - update-types: - - "patch" - prod-minor-updates: - dependency-type: "production" - update-types: - - "minor" - - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - groups: - patch-updates: - update-types: - - "patch" - minor-updates: - update-types: - - "minor" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - - - package-ecosystem: "gomod" - directory: "/install" - schedule: - interval: "daily" - groups: - dev-patch-updates: - dependency-type: "development" - update-types: - - "patch" - dev-minor-updates: - dependency-type: "development" - update-types: - - "minor" - prod-patch-updates: - dependency-type: "production" - update-types: - - "patch" - prod-minor-updates: - dependency-type: "production" - update-types: - - "minor" \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 0d2008f1..bc581582 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -3,22 +3,22 @@ name: CI/CD Pipeline on: push: tags: - - "[0-9]+.[0-9]+.[0-9]+" + - "*" jobs: release: name: Build and Release - runs-on: amd64-runner + runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} @@ -28,9 +28,9 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v4 with: - go-version: 1.24 + go-version: 1.23.0 - name: Update version in package.json run: | diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml deleted file mode 100644 index 1a01f1c4..00000000 --- a/.github/workflows/linting.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: ESLint - -on: - pull_request: - paths: - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '.eslintrc*' - - 'package.json' - - 'yarn.lock' - - 'pnpm-lock.yaml' - - 'package-lock.json' - -jobs: - Linter: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install dependencies - 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 diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 4a574d91..d0ca1685 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -14,7 +14,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v10 + - uses: actions/stale@v9 with: days-before-stale: 14 days-before-close: 14 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 3d121f68..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Run Tests - -on: - pull_request: - branches: - - main - - dev - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Copy config file - run: cp config/config.example.yml config/config.yml - - - name: Install dependencies - run: npm ci - - - name: Create database 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 - - - 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 & - - - name: Wait for app availability - run: | - for i in {1..5}; do - if curl --silent --fail http://localhost:3002/auth/login; then - echo "App is up" - exit 0 - fi - echo "Waiting for the app... attempt $i" - sleep 5 - done - echo "App failed to start" - exit 1 - - - name: Build Docker image sqlite - run: make build-sqlite - - - name: Build Docker image pg - run: make build-pg diff --git a/.gitignore b/.gitignore index 1f4483cf..cd73cef1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ yarn-error.log* next-env.d.ts *.db *.sqlite -!Dockerfile.sqlite *.sqlite3 *.log .machinelogs*.json @@ -26,14 +25,6 @@ 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 -config/key dist .dist installer @@ -41,11 +32,4 @@ installer bin .secrets test_event.json -.idea/ -public/branding -server/db/index.ts -postgres/ -dynamic/ -*.mmdb -scratch/ -/tmp +.idea/ \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 2bd5a0a9..209e3ef4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +20 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bd2bc67..44acedb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,11 @@ Contributions are welcome! Please see the contribution and local development guide on the docs page before getting started: -https://docs.digpangolin.com/development/contributing +https://docs.fossorial.io/development + +For ideas about what features to work on and our future plans, please see the roadmap: + +https://docs.fossorial.io/roadmap ### Licensing Considerations diff --git a/Dockerfile b/Dockerfile index 0ae66107..6b2c55aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,55 +1,34 @@ -FROM node:22-alpine AS builder +FROM node:20-alpine AS builder WORKDIR /app -ARG DATABASE=sqlite - -# COPY package.json package-lock.json ./ -COPY package*.json ./ -RUN apk update && apk add python3 -RUN npm ci +COPY package.json package-lock.json ./ +RUN npm install COPY . . -RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts +RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init -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 npm run build -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:cli - -FROM node:22-alpine AS runner +FROM node:20-alpine AS runner WORKDIR /app # Curl used for the health checks -RUN apk add --no-cache curl tzdata +RUN apk add --no-cache curl # COPY package.json package-lock.json ./ -COPY package*.json ./ - -RUN npm ci --omit=dev && npm cache clean --force +COPY package.json ./ +RUN npm install --omit=dev && npm cache clean --force COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/dist ./dist COPY --from=builder /app/init ./dist/init -COPY ./cli/wrapper.sh /usr/local/bin/pangctl -RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs - COPY server/db/names.json ./dist/names.json + COPY public ./public -CMD ["npm", "run", "start"] +CMD ["npm", "start"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index c40775c2..00000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:22-alpine - -WORKDIR /app - -COPY package*.json ./ - -# Install dependencies -RUN npm ci - -# Copy source code -COPY . . - -# Use tsx watch for development with hot reload -CMD ["npm", "run", "dev"] diff --git a/LICENSE b/LICENSE index 0ad25db4..0e38f56b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +Copyright (c) 2025 Fossorial, LLC. + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/Makefile b/Makefile index c90bd180..793a3481 100644 --- a/Makefile +++ b/Makefile @@ -1,48 +1,10 @@ -.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="; \ + echo "Error: tag is required. Usage: make build-all tag="; \ exit 1; \ fi - 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 . + docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push . + docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push . build-arm: docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . @@ -50,11 +12,8 @@ build-arm: build-x86: docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . -build-sqlite: - docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest . - -build-pg: - docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest . +build: + docker build -t fosrl/pangolin:latest . test: docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest diff --git a/README.md b/README.md index f7f4ec30..e513a136 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,152 @@
-

- - - - Pangolin Logo - - -

+

pangolin

+ +[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/) +[![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) + +
+ +

Tunneled Mesh Reverse Proxy Server with Access Control

+
+ +_Your own self-hosted zero trust tunnel._ +
- + Website | - - Documentation + + Install Guide | - + Contact Us
-
+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. -[![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) -[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) +Preview -
- -

- - Start testing Pangolin at pangolin.fossorial.io - -

- -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. +_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._ This is a fork of Pangolin with all proprietary code removed. Proprietary and paywalled features will be reimplemented under the AGPL license. -## Installation - -Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick-install) for how to install and set up Pangolin. - -## Deployment Options - -| | 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. | - ## Key Features -Pangolin packages everything you need for seamless application access and exposure into one cohesive platform. +### Reverse Proxy Through WireGuard Tunnel -| | | -|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| -| **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. | | +- Expose private resources on your network **without opening ports** (firewall punching). +- Secure and easy to configure site-to-site 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. -## Get Started +### Identity & Access Management -### Check out the docs +- 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. +- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others. + - Auto-provision users and roles from your IdP. -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. +### Simple Dashboard UI -### Sign up and try now +- Manage sites, users, and roles with a clean and intuitive UI. +- Monitor site usage and connectivity. +- Light and dark mode options. +- Mobile friendly. -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. +### Easy Deployment + +- Run on any cloud provider or on-premises. +- **Docker Compose based setup** for simplified deployment. +- Future-proof installation script for streamlined setup and feature additions. +- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience. +- Use the API to create custom integrations and scripts. + - Fine-grained access control to the API via scoped API keys. + - Comprehensive Swagger documentation for the API. + +### Modular Design + +- 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](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. + +Collage + +## Deployment and Usage Example + +1. **Deploy the Central Server**: + + - Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs. + +> [!TIP] +> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal! +> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone. + +2. **Domain Configuration**: + + - Point your domain name to the VPS and configure Pangolin with your preferred settings. + +3. **Connect Private Sites**: + + - Install Newt or use another WireGuard client on private sites. + - Automatically establish a connection from these sites to the central server. + +4. **Expose Resources**: + + - Add resources to the central server and configure access control rules. + - Access these resources securely from anywhere. + +**Use Case Example - Bypassing Port Restrictions in Home Lab**: + Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity. + +**Use Case Example - Deploying Services For Your Business**: +You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud. + +**Use Case Example - IoT Networks**: + IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups. + +## Similar Projects and Inspirations + +**Cloudflare Tunnels**: + A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure. + +**Authelia**: + This inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management. + +## Project Development / Roadmap + +> [!NOTE] +> Pangolin is under heavy development. The roadmap is subject to change as we fix bugs, add new features, and make improvements. + +View the [project board](https://github.com/orgs/fosrl/projects/1) for more detailed info. ## Licensing -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). +Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). ## Contributions 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. +For all feature requests, or other ideas, please use the [Discussions](https://github.com/orgs/fosrl/discussions) section. diff --git a/blueprint.py b/blueprint.py deleted file mode 100644 index 93d21da5..00000000 --- a/blueprint.py +++ /dev/null @@ -1,72 +0,0 @@ -import requests -import yaml -import json -import base64 - -# The file path for the YAML file to be read -# You can change this to the path of your YAML file -YAML_FILE_PATH = 'blueprint.yaml' - -# The API endpoint and headers from the curl request -API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint' -HEADERS = { - 'accept': '*/*', - 'Authorization': 'Bearer ', - 'Content-Type': 'application/json' -} - -def convert_and_send(file_path, url, headers): - """ - Reads a YAML file, converts its content to a JSON payload, - and sends it via a PUT request to a specified URL. - """ - try: - # Read the YAML file content - with open(file_path, 'r') as file: - yaml_content = file.read() - - # Parse the YAML string to a Python dictionary - # This will be used to ensure the YAML is valid before sending - parsed_yaml = yaml.safe_load(yaml_content) - - # convert the parsed YAML to a JSON string - json_payload = json.dumps(parsed_yaml) - print("Converted JSON payload:") - print(json_payload) - - # Encode the JSON string to Base64 - encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8') - - # Create the final payload with the base64 encoded data - final_payload = { - "blueprint": encoded_json - } - - print("Sending the following Base64 encoded JSON payload:") - print(final_payload) - print("-" * 20) - - # Make the PUT request with the base64 encoded payload - response = requests.put(url, headers=headers, json=final_payload) - - # Print the API response for debugging - print(f"API Response Status Code: {response.status_code}") - print("API Response Content:") - print(response.text) - - # Raise an exception for bad status codes (4xx or 5xx) - response.raise_for_status() - - except FileNotFoundError: - print(f"Error: The file '{file_path}' was not found.") - except yaml.YAMLError as e: - print(f"Error parsing YAML file: {e}") - except requests.exceptions.RequestException as e: - print(f"An error occurred during the API request: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - -# Run the function -if __name__ == "__main__": - convert_and_send(YAML_FILE_PATH, API_URL, HEADERS) - diff --git a/blueprint.yaml b/blueprint.yaml deleted file mode 100644 index 03c51521..00000000 --- a/blueprint.yaml +++ /dev/null @@ -1,69 +0,0 @@ -client-resources: - client-resource-nice-id-uno: - name: this is my resource - protocol: tcp - proxy-port: 3001 - hostname: localhost - internal-port: 3000 - site: lively-yosemite-toad - client-resource-nice-id-duce: - name: this is my resource - protocol: udp - proxy-port: 3000 - hostname: localhost - internal-port: 3000 - site: lively-yosemite-toad - -proxy-resources: - resource-nice-id-uno: - name: this is my resource - protocol: http - full-domain: duce.test.example.com - host-header: example.com - tls-server-name: example.com - # auth: - # pincode: 123456 - # password: sadfasdfadsf - # sso-enabled: true - # sso-roles: - # - Member - # sso-users: - # - owen@fossorial.io - # whitelist-users: - # - owen@fossorial.io - headers: - - name: X-Example-Header - value: example-value - - name: X-Another-Header - value: another-value - rules: - - action: allow - match: ip - value: 1.1.1.1 - - action: deny - match: cidr - value: 2.2.2.2/32 - - action: pass - match: path - value: /admin - targets: - - site: lively-yosemite-toad - path: /path - pathMatchType: prefix - hostname: localhost - method: http - port: 8000 - - site: slim-alpine-chipmunk - hostname: localhost - path: /yoman - pathMatchType: exact - method: http - port: 8001 - resource-nice-id-duce: - name: this is other resource - protocol: tcp - proxy-port: 3000 - targets: - - site: lively-yosemite-toad - hostname: localhost - port: 3000 \ No newline at end of file diff --git a/bruno/API Keys/Create API Key.bru b/bruno/API Keys/Create API Key.bru deleted file mode 100644 index 009b4b04..00000000 --- a/bruno/API Keys/Create API Key.bru +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 9285f788..00000000 --- a/bruno/API Keys/Delete API Key.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ae5b721e..00000000 --- a/bruno/API Keys/List API Key Actions.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 468e964b..00000000 --- a/bruno/API Keys/List Org API Keys.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 8ef31b68..00000000 --- a/bruno/API Keys/List Root API Keys.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 54a35c43..00000000 --- a/bruno/API Keys/Set API Key Actions.bru +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 3f0676c5..00000000 --- a/bruno/API Keys/Set API Key Orgs.bru +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index bb8cd5c7..00000000 --- a/bruno/API Keys/folder.bru +++ /dev/null @@ -1,3 +0,0 @@ -meta { - name: API Keys -} diff --git a/bruno/Auth/login.bru b/bruno/Auth/login.bru index 2b88066b..3825a252 100644 --- a/bruno/Auth/login.bru +++ b/bruno/Auth/login.bru @@ -5,14 +5,14 @@ meta { } post { - url: http://localhost:4000/api/v1/auth/login + url: http://localhost:3000/api/v1/auth/login body: json auth: none } body:json { { - "email": "owen@fossorial.io", + "email": "admin@fosrl.io", "password": "Password123!" } } diff --git a/bruno/Auth/logout.bru b/bruno/Auth/logout.bru index 623cd47f..7dd134cc 100644 --- a/bruno/Auth/logout.bru +++ b/bruno/Auth/logout.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://localhost:4000/api/v1/auth/logout + url: http://localhost:3000/api/v1/auth/logout body: none auth: none } diff --git a/bruno/Clients/createClient.bru b/bruno/Clients/createClient.bru deleted file mode 100644 index 7577bb28..00000000 --- a/bruno/Clients/createClient.bru +++ /dev/null @@ -1,22 +0,0 @@ -meta { - name: createClient - type: http - seq: 1 -} - -put { - url: http://localhost:3000/api/v1/site/1/client - body: json - auth: none -} - -body:json { - { - "siteId": 1, - "name": "test", - "type": "olm", - "subnet": "100.90.129.4/30", - "olmId": "029yzunhx6nh3y5", - "secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6" - } -} diff --git a/bruno/Clients/pickClientDefaults.bru b/bruno/Clients/pickClientDefaults.bru deleted file mode 100644 index 61509c11..00000000 --- a/bruno/Clients/pickClientDefaults.bru +++ /dev/null @@ -1,11 +0,0 @@ -meta { - name: pickClientDefaults - type: http - seq: 2 -} - -get { - url: http://localhost:3000/api/v1/site/1/pick-client-defaults - body: none - auth: none -} diff --git a/bruno/IDP/Create OIDC Provider.bru b/bruno/IDP/Create OIDC Provider.bru deleted file mode 100644 index 23e807cf..00000000 --- a/bruno/IDP/Create OIDC Provider.bru +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 90443096..00000000 --- a/bruno/IDP/Generate OIDC URL.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index fc136915..00000000 --- a/bruno/IDP/folder.bru +++ /dev/null @@ -1,3 +0,0 @@ -meta { - name: IDP -} diff --git a/bruno/Internal/Traefik Config.bru b/bruno/Internal/Traefik Config.bru deleted file mode 100644 index 9fc1c1dc..00000000 --- a/bruno/Internal/Traefik Config.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 702931ec..00000000 --- a/bruno/Internal/folder.bru +++ /dev/null @@ -1,3 +0,0 @@ -meta { - name: Internal -} diff --git a/bruno/Remote Exit Node/createRemoteExitNode.bru b/bruno/Remote Exit Node/createRemoteExitNode.bru deleted file mode 100644 index 1c749a31..00000000 --- a/bruno/Remote Exit Node/createRemoteExitNode.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 16286ec8..00000000 --- a/bruno/Test.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 f0ed66b3..f19d936a 100644 --- a/bruno/bruno.json +++ b/bruno/bruno.json @@ -1,6 +1,6 @@ { "version": "1", - "name": "Pangolin Saas", + "name": "Pangolin", "type": "collection", "ignore": [ "node_modules", diff --git a/cli/commands/resetUserSecurityKeys.ts b/cli/commands/resetUserSecurityKeys.ts deleted file mode 100644 index fdae0ebd..00000000 --- a/cli/commands/resetUserSecurityKeys.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { CommandModule } from "yargs"; -import { db, users, securityKeys } from "@server/db"; -import { eq } from "drizzle-orm"; - -type ResetUserSecurityKeysArgs = { - email: string; -}; - -export const resetUserSecurityKeys: CommandModule< - {}, - ResetUserSecurityKeysArgs -> = { - command: "reset-user-security-keys", - describe: - "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", - builder: (yargs) => { - return yargs.option("email", { - type: "string", - demandOption: true, - describe: "User email address" - }); - }, - handler: async (argv: { email: string }) => { - try { - const { email } = argv; - - console.log(`Looking for user with email: ${email}`); - - // Find the user by email - const [user] = await db - .select() - .from(users) - .where(eq(users.email, email)) - .limit(1); - - if (!user) { - console.error(`User with email '${email}' not found`); - process.exit(1); - } - - console.log(`Found user: ${user.email} (ID: ${user.userId})`); - - // Check if user has any security keys - const userSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, user.userId)); - - if (userSecurityKeys.length === 0) { - console.log(`User '${email}' has no security keys to reset`); - process.exit(0); - } - - console.log( - `Found ${userSecurityKeys.length} security key(s) for user '${email}'` - ); - - // Delete all security keys for the user - await db - .delete(securityKeys) - .where(eq(securityKeys.userId, user.userId)); - - console.log(`Successfully reset security keys for user '${email}'`); - console.log(`Deleted ${userSecurityKeys.length} security key(s)`); - - process.exit(0); - } catch (error) { - console.error("Error:", error); - process.exit(1); - } - } -}; diff --git a/cli/commands/setAdminCredentials.ts b/cli/commands/setAdminCredentials.ts deleted file mode 100644 index 91a6bcf7..00000000 --- a/cli/commands/setAdminCredentials.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { CommandModule } from "yargs"; -import { hashPassword, verifyPassword } from "@server/auth/password"; -import { db, resourceSessions, sessions } from "@server/db"; -import { users } from "@server/db"; -import { eq, inArray } from "drizzle-orm"; -import moment from "moment"; -import { fromError } from "zod-validation-error"; -import { passwordSchema } from "@server/auth/passwordSchema"; -import { UserType } from "@server/types/UserTypes"; -import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; - -type SetAdminCredentialsArgs = { - email: string; - password: string; -}; - -export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = { - command: "set-admin-credentials", - describe: "Set the server admin credentials", - builder: (yargs) => { - return yargs - .option("email", { - type: "string", - demandOption: true, - describe: "Admin email address" - }) - .option("password", { - type: "string", - demandOption: true, - describe: "Admin password" - }); - }, - handler: async (argv: { email: string; password: string }) => { - try { - const { password } = argv; - let { email } = argv; - email = email.trim().toLowerCase(); - - const parsed = passwordSchema.safeParse(password); - - if (!parsed.success) { - throw Error( - `Invalid server admin password: ${fromError(parsed.error).toString()}` - ); - } - - const passwordHash = await hashPassword(password); - - await db.transaction(async (trx) => { - try { - const [existing] = await trx - .select() - .from(users) - .where(eq(users.serverAdmin, true)); - - if (existing) { - const passwordChanged = !(await verifyPassword( - password, - existing.passwordHash! - )); - - if (passwordChanged) { - await trx - .update(users) - .set({ passwordHash }) - .where(eq(users.userId, existing.userId)); - - await invalidateAllSessions(existing.userId); - console.log("Server admin password updated"); - } - - if (existing.email !== email) { - await trx - .update(users) - .set({ email, username: email }) - .where(eq(users.userId, existing.userId)); - - console.log("Server admin email updated"); - } - } else { - const userId = generateId(15); - - await trx.update(users).set({ serverAdmin: false }); - - await db.insert(users).values({ - userId: userId, - email: email, - type: UserType.Internal, - username: email, - passwordHash, - dateCreated: moment().toISOString(), - serverAdmin: true, - emailVerified: true - }); - - console.log("Server admin created"); - } - } catch (e) { - console.error("Failed to set admin credentials", e); - trx.rollback(); - throw e; - } - }); - - console.log("Admin credentials updated successfully"); - process.exit(0); - } catch (error) { - console.error("Error:", error); - process.exit(1); - } - } -}; - -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)); - await trx.delete(resourceSessions).where( - inArray( - resourceSessions.userSessionId, - userSessions.map((s) => s.sessionId) - ) - ); - await trx.delete(sessions).where(eq(sessions.userId, userId)); - }); - } catch (e) { - console.log("Failed to all invalidate user sessions", e); - } -} - -const random: RandomReader = { - read(bytes: Uint8Array): void { - crypto.getRandomValues(bytes); - } -}; - -export function generateId(length: number): string { - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - return generateRandomString(random, alphabet, length); -} diff --git a/cli/index.ts b/cli/index.ts deleted file mode 100644 index f9e884cc..00000000 --- a/cli/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node - -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -import { setAdminCredentials } from "@cli/commands/setAdminCredentials"; -import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys"; - -yargs(hideBin(process.argv)) - .scriptName("pangctl") - .command(setAdminCredentials) - .command(resetUserSecurityKeys) - .demandCommand() - .help().argv; diff --git a/cli/wrapper.sh b/cli/wrapper.sh deleted file mode 100644 index 0f65092b..00000000 --- a/cli/wrapper.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd /app/ -./dist/cli.mjs "$@" diff --git a/config/config.example.yml b/config/config.example.yml index fcb7edde..7b5c144d 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,28 +1,54 @@ # To see all available options, please visit the docs: -# https://docs.digpangolin.com/self-host/advanced/config-file +# https://docs.fossorial.io/Pangolin/Configuration/config app: - dashboard_url: http://localhost:3002 - log_level: debug + dashboard_url: "http://localhost:3002" + log_level: "info" + save_logs: false domains: - domain1: - base_domain: example.com + domain1: + base_domain: "example.com" + cert_resolver: "letsencrypt" server: - secret: my_secret_key + external_port: 3000 + internal_port: 3001 + next_port: 3002 + internal_hostname: "pangolin" + session_cookie_name: "p_session_token" + resource_access_token_param: "p_token" + secret: "your_secret_key_here" + resource_access_token_headers: + id: "P-Access-Token-Id" + token: "P-Access-Token" + resource_session_request_param: "p_session_request" + +traefik: + http_entrypoint: "web" + https_entrypoint: "websecure" gerbil: - base_endpoint: example.com + start_port: 51820 + base_endpoint: "localhost" + block_size: 24 + site_block_size: 30 + subnet_group: 100.89.137.0/20 + use_subdomain: true -orgs: - block_size: 24 - subnet_group: 100.90.137.0/20 +rate_limits: + global: + window_minutes: 1 + max_requests: 500 + +users: + server_admin: + email: "admin@example.com" + password: "Password123!" flags: - require_email_verification: false - disable_signup_without_invite: true - disable_user_create_org: true - allow_raw_resources: true - enable_integration_api: true - enable_clients: true + require_email_verification: false + disable_signup_without_invite: true + disable_user_create_org: true + allow_raw_resources: true + allow_base_domain_resources: true diff --git a/config/traefik/dynamic_config.yml b/config/traefik/dynamic_config.yml deleted file mode 100644 index 8465a9cf..00000000 --- a/config/traefik/dynamic_config.yml +++ /dev/null @@ -1,46 +0,0 @@ -http: - middlewares: - redirect-to-https: - redirectScheme: - scheme: https - - routers: - # HTTP to HTTPS redirect router - main-app-router-redirect: - rule: "Host(`{{.DashboardDomain}}`)" - service: next-service - entryPoints: - - web - middlewares: - - redirect-to-https - - # Next.js router (handles everything except API and WebSocket paths) - next-router: - rule: "Host(`{{.DashboardDomain}}`)" - service: next-service - priority: 10 - entryPoints: - - websecure - tls: - certResolver: letsencrypt - - # API router (handles /api/v1 paths) - api-router: - rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)" - service: api-service - priority: 100 - entryPoints: - - websecure - tls: - certResolver: letsencrypt - - services: - next-service: - loadBalancer: - servers: - - url: "http://pangolin:3002" # Next.js server - - api-service: - loadBalancer: - servers: - - url: "http://pangolin:3000" # API/WebSocket server diff --git a/config/traefik/traefik_config.yml b/config/traefik/traefik_config.yml deleted file mode 100644 index 43ea97be..00000000 --- a/config/traefik/traefik_config.yml +++ /dev/null @@ -1,34 +0,0 @@ -api: - insecure: true - dashboard: true - -providers: - file: - directory: "/var/dynamic" - watch: true - -experimental: - plugins: - badger: - moduleName: "github.com/fosrl/badger" - version: "v1.2.0" - -log: - level: "DEBUG" - format: "common" - maxSize: 100 - maxBackups: 3 - maxAge: 3 - compress: true - -entryPoints: - web: - address: ":80" - websecure: - address: ":9443" - transport: - respondingTimeouts: - readTimeout: "30m" - -serversTransport: - insecureSkipVerify: true diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index 6787087e..00000000 --- a/crowdin.yml +++ /dev/null @@ -1,3 +0,0 @@ -files: - - source: /messages/en-US.json - translation: /messages/%locale%.json \ No newline at end of file diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 21a5134f..973d27fa 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -20,9 +20,10 @@ services: pangolin: condition: service_healthy command: - - --reachableAt=http://gerbil:3004 + - --reachableAt=http://gerbil:3003 - --generateAndSaveKeyTo=/var/config/key - - --remoteConfig=http://pangolin:3001/api/v1/ + - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config + - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth volumes: - ./config/:/var/config cap_add: @@ -30,12 +31,11 @@ services: - SYS_MODULE ports: - 51820:51820/udp - - 21820:21820/udp - 443:443 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode traefik: - image: traefik:v3.5 + image: traefik:v3.3.3 container_name: traefik restart: unless-stopped network_mode: service:gerbil # Ports appear on the gerbil service @@ -51,5 +51,4 @@ services: networks: default: driver: bridge - name: pangolin - enable_ipv6: true \ No newline at end of file + name: pangolin \ No newline at end of file diff --git a/docker-compose.pgr.yml b/docker-compose.pgr.yml deleted file mode 100644 index 2a45f129..00000000 --- a/docker-compose.pgr.yml +++ /dev/null @@ -1,21 +0,0 @@ -services: - # PostgreSQL Service - db: - image: postgres:17 # Use the PostgreSQL 17 image - container_name: dev_postgres # Name your PostgreSQL container - environment: - POSTGRES_DB: postgres # Default database name - POSTGRES_USER: postgres # Default user - POSTGRES_PASSWORD: password # Default password (change for production!) - volumes: - - ./config/postgres:/var/lib/postgresql/data - ports: - - "5432:5432" # Map host port 5432 to container port 5432 - restart: no - - 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.yml b/docker-compose.yml deleted file mode 100644 index 469f9b4c..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -services: - # Development application service - app: - build: - context: . - dockerfile: Dockerfile.dev - container_name: dev_pangolin - ports: - - "3000:3000" - - "3001:3001" - - "3002:3002" - - "3003:3003" - environment: - - NODE_ENV=development - - ENVIRONMENT=dev - volumes: - # Mount source code for hot reload - - ./src:/app/src - - ./server:/app/server - - ./public:/app/public - - ./messages:/app/messages - - ./components.json:/app/components.json - - ./next.config.mjs:/app/next.config.mjs - - ./tsconfig.json:/app/tsconfig.json - - ./tailwind.config.js:/app/tailwind.config.js - - ./postcss.config.mjs:/app/postcss.config.mjs - - ./eslint.config.js:/app/eslint.config.js - - ./config:/app/config - restart: no diff --git a/drizzle.sqlite.config.ts b/drizzle.config.ts similarity index 77% rename from drizzle.sqlite.config.ts rename to drizzle.config.ts index 4912c256..dcfc55c6 100644 --- a/drizzle.sqlite.config.ts +++ b/drizzle.config.ts @@ -2,13 +2,9 @@ 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: schema, + schema: path.join("server", "db", "schemas"), out: path.join("server", "migrations"), verbose: true, dbCredentials: { diff --git a/drizzle.pg.config.ts b/drizzle.pg.config.ts deleted file mode 100644 index febd5f45..00000000 --- a/drizzle.pg.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from "drizzle-kit"; -import path from "path"; - -const schema = [ - path.join("server", "db", "pg", "schema"), -]; - -export default defineConfig({ - dialect: "postgresql", - schema: schema, - out: path.join("server", "migrations"), - verbose: true, - dbCredentials: { - url: process.env.DATABASE_URL as string - } -}); diff --git a/esbuild.mjs b/esbuild.mjs index 8086a77e..48a2fb31 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -52,7 +52,7 @@ esbuild bundle: true, outfile: argv.out, format: "esm", - minify: false, + minify: true, banner: { js: banner, }, @@ -63,8 +63,8 @@ esbuild packagePath: getPackagePaths(), }), ], - sourcemap: "inline", - target: "node22", + sourcemap: true, + target: "node20", }) .then(() => { console.log("Build completed successfully"); diff --git a/eslint.config.js b/eslint.config.js index dfc194bc..71dc862c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,19 +1,9 @@ -import tseslint from 'typescript-eslint'; - -export default tseslint.config({ - files: ["**/*.{ts,tsx,js,jsx}"], - languageOptions: { - parser: tseslint.parser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true - } +// eslint.config.js +export default [ + { + rules: { + semi: "error", + "prefer-const": "error" + } } - }, - rules: { - "semi": "error", - "prefer-const": "warn" - } -}); \ No newline at end of file +]; diff --git a/install/Makefile b/install/Makefile index 8b65cadd..9bde02cf 100644 --- a/install/Makefile +++ b/install/Makefile @@ -1,5 +1,4 @@ all: update-versions go-build-release put-back -dev-all: dev-update-versions dev-build dev-clean go-build-release: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64 @@ -12,12 +11,6 @@ clean: update-versions: @echo "Fetching latest versions..." cp main.go main.go.bak && \ - $(MAKE) dev-update-versions - -put-back: - mv main.go.bak main.go - -dev-update-versions: PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \ GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \ BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \ @@ -27,11 +20,5 @@ dev-update-versions: sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \ echo "Updated main.go with latest versions" -dev-build: go-build-release - -dev-clean: - @echo "Restoring version values ..." - sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \ - sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \ - sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go - @echo "Restored version strings in main.go" +put-back: + mv main.go.bak main.go \ No newline at end of file diff --git a/install/config.go b/install/config.go index e75dd50d..3be62601 100644 --- a/install/config.go +++ b/install/config.go @@ -37,28 +37,15 @@ type DynamicConfig struct { } `yaml:"http"` } -// TraefikConfigValues holds the extracted configuration values -type TraefikConfigValues struct { +// ConfigValues holds the extracted configuration values +type ConfigValues struct { DashboardDomain string LetsEncryptEmail string BadgerVersion string } -// AppConfig represents the app section of the config.yml -type AppConfig struct { - App struct { - DashboardURL string `yaml:"dashboard_url"` - LogLevel string `yaml:"log_level"` - } `yaml:"app"` -} - -type AppConfigValues struct { - DashboardURL string - LogLevel string -} - // ReadTraefikConfig reads and extracts values from Traefik configuration files -func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) { +func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) { // Read main config file mainConfigData, err := os.ReadFile(mainConfigPath) if err != nil { @@ -70,33 +57,48 @@ func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) { return nil, fmt.Errorf("error parsing main config file: %w", err) } + // Read dynamic config file + dynamicConfigData, err := os.ReadFile(dynamicConfigPath) + if err != nil { + return nil, fmt.Errorf("error reading dynamic config file: %w", err) + } + + var dynamicConfig DynamicConfig + if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil { + return nil, fmt.Errorf("error parsing dynamic config file: %w", err) + } + // Extract values - values := &TraefikConfigValues{ + values := &ConfigValues{ BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version, LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email, } + // Extract DashboardDomain from router rules + // Look for it in the main router rules + for _, router := range dynamicConfig.HTTP.Routers { + if router.Rule != "" { + // Extract domain from Host(`mydomain.com`) + if domain := extractDomainFromRule(router.Rule); domain != "" { + values.DashboardDomain = domain + break + } + } + } + return values, nil } -func ReadAppConfig(configPath string) (*AppConfigValues, error) { - // Read config file - configData, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("error reading config file: %w", err) +// extractDomainFromRule extracts the domain from a router rule +func extractDomainFromRule(rule string) string { + // Look for the Host(`mydomain.com`) pattern + if start := findPattern(rule, "Host(`"); start != -1 { + end := findPattern(rule[start:], "`)") + if end != -1 { + return rule[start+6 : start+end] + } } - - var appConfig AppConfig - if err := yaml.Unmarshal(configData, &appConfig); err != nil { - return nil, fmt.Errorf("error parsing config file: %w", err) - } - - values := &AppConfigValues{ - DashboardURL: appConfig.App.DashboardURL, - LogLevel: appConfig.App.LogLevel, - } - - return values, nil + return "" } // findPattern finds the start of a pattern in a string diff --git a/install/config/config.yml b/install/config/config.yml index 7e73aa62..f7d4552d 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,13 +1,10 @@ # To see all available options, please visit the docs: -# https://docs.digpangolin.com/ - -gerbil: - start_port: 51820 - base_endpoint: "{{.DashboardDomain}}" +# https://docs.fossorial.io/Pangolin/Configuration/config app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" + save_logs: false domains: domain1: @@ -15,13 +12,40 @@ domains: cert_resolver: "letsencrypt" server: - secret: "{{.Secret}}" + 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: "p_session_request" + secret: {{.Secret}} cors: origins: ["https://{{.DashboardDomain}}"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] - allowed_headers: ["X-CSRF-Token", "Content-Type"] + headers: ["X-CSRF-Token", "Content-Type"] credentials: false - {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} + +traefik: + cert_resolver: "letsencrypt" + http_entrypoint: "web" + https_entrypoint: "websecure" + +gerbil: + start_port: 51820 + base_endpoint: "{{.DashboardDomain}}" + use_subdomain: false + block_size: 24 + site_block_size: 30 + subnet_group: 100.89.137.0/20 + +rate_limits: + global: + window_minutes: 1 + max_requests: 500 {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" @@ -30,9 +54,14 @@ email: smtp_pass: "{{.EmailSMTPPass}}" no_reply: "{{.EmailNoReply}}" {{end}} +users: + server_admin: + email: "{{.AdminUserEmail}}" + password: "{{.AdminUserPassword}}" + flags: require_email_verification: {{.EnableEmail}} - disable_signup_without_invite: true - disable_user_create_org: false + disable_signup_without_invite: {{.DisableSignupWithoutInvite}} + disable_user_create_org: {{.DisableUserCreateOrg}} allow_raw_resources: true -{{end}} + allow_base_domain_resources: true diff --git a/install/config/crowdsec/docker-compose.yml b/install/config/crowdsec/docker-compose.yml index 17289ef2..28470d14 100644 --- a/install/config/crowdsec/docker-compose.yml +++ b/install/config/crowdsec/docker-compose.yml @@ -1,6 +1,6 @@ services: crowdsec: - image: docker.io/crowdsecurity/crowdsec:latest + image: crowdsecurity/crowdsec:latest container_name: crowdsec environment: GID: "1000" diff --git a/install/config/crowdsec/profiles.yaml b/install/config/crowdsec/profiles.yaml index 5781cf62..3796b47f 100644 --- a/install/config/crowdsec/profiles.yaml +++ b/install/config/crowdsec/profiles.yaml @@ -22,4 +22,4 @@ filters: decisions: - type: ban duration: 4h -on_success: break +on_success: break \ No newline at end of file diff --git a/install/config/crowdsec/traefik_config.yml b/install/config/crowdsec/traefik_config.yml index 198693ef..f16e9c60 100644 --- a/install/config/crowdsec/traefik_config.yml +++ b/install/config/crowdsec/traefik_config.yml @@ -16,15 +16,11 @@ experimental: version: "{{.BadgerVersion}}" crowdsec: # CrowdSec plugin configuration added moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - version: "v1.4.4" + version: "v1.4.2" log: level: "INFO" format: "json" # Log format changed to json for better parsing - maxSize: 100 - maxBackups: 3 - maxAge: 3 - compress: true accessLog: # We enable access logs as json filePath: "/var/log/traefik/access.log" diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index b507e914..6c1a3755 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -1,7 +1,7 @@ name: pangolin services: pangolin: - image: docker.io/fosrl/pangolin:{{.PangolinVersion}} + image: fosrl/pangolin:{{.PangolinVersion}} container_name: pangolin restart: unless-stopped volumes: @@ -13,16 +13,17 @@ services: retries: 15 {{if .InstallGerbil}} gerbil: - image: docker.io/fosrl/gerbil:{{.GerbilVersion}} + image: fosrl/gerbil:{{.GerbilVersion}} container_name: gerbil restart: unless-stopped depends_on: pangolin: condition: service_healthy command: - - --reachableAt=http://gerbil:3004 + - --reachableAt=http://gerbil:3003 - --generateAndSaveKeyTo=/var/config/key - - --remoteConfig=http://pangolin:3001/api/v1/ + - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config + - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth volumes: - ./config/:/var/config cap_add: @@ -30,12 +31,11 @@ services: - SYS_MODULE ports: - 51820:51820/udp - - 21820:21820/udp - - 443:443 - - 80:80 + - 443:443 # Port for traefik because of the network_mode + - 80:80 # Port for traefik because of the network_mode {{end}} traefik: - image: docker.io/traefik:v3.5 + image: traefik:v3.3.6 container_name: traefik restart: unless-stopped {{if .InstallGerbil}} @@ -58,5 +58,4 @@ services: networks: default: driver: bridge - name: pangolin -{{if .EnableIPv6}} enable_ipv6: true{{end}} \ No newline at end of file + name: pangolin \ No newline at end of file diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index a9693ce6..40507c24 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -18,10 +18,6 @@ experimental: log: level: "INFO" format: "common" - maxSize: 100 - maxBackups: 3 - maxAge: 3 - compress: true certificatesResolvers: letsencrypt: @@ -46,6 +42,3 @@ entryPoints: serversTransport: insecureSkipVerify: true - -ping: - entryPoint: "web" \ No newline at end of file diff --git a/install/containers.go b/install/containers.go deleted file mode 100644 index cea3a6ef..00000000 --- a/install/containers.go +++ /dev/null @@ -1,332 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "os/user" - "runtime" - "strconv" - "strings" - "time" -) - -func waitForContainer(containerName string, containerType SupportedContainer) error { - maxAttempts := 30 - retryInterval := time.Second * 2 - - for attempt := 0; attempt < maxAttempts; attempt++ { - // Check if container is running - cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName) - var out bytes.Buffer - cmd.Stdout = &out - - if err := cmd.Run(); err != nil { - // If the container doesn't exist or there's another error, wait and retry - time.Sleep(retryInterval) - continue - } - - isRunning := strings.TrimSpace(out.String()) == "true" - if isRunning { - return nil - } - - // Container exists but isn't running yet, wait and retry - time.Sleep(retryInterval) - } - - return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) -} - -func installDocker() error { - // Detect Linux distribution - cmd := exec.Command("cat", "/etc/os-release") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to detect Linux distribution: %v", err) - } - osRelease := string(output) - - // Detect system architecture - archCmd := exec.Command("uname", "-m") - archOutput, err := archCmd.Output() - if err != nil { - return fmt.Errorf("failed to detect system architecture: %v", err) - } - arch := strings.TrimSpace(string(archOutput)) - - // Map architecture to Docker's architecture naming - var dockerArch string - switch arch { - case "x86_64": - dockerArch = "amd64" - case "aarch64": - dockerArch = "arm64" - default: - return fmt.Errorf("unsupported architecture: %s", arch) - } - - var installCmd *exec.Cmd - switch { - case strings.Contains(osRelease, "ID=ubuntu"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - apt-get update && - apt-get install -y apt-transport-https ca-certificates curl software-properties-common && - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && - echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && - apt-get update && - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, dockerArch)) - case strings.Contains(osRelease, "ID=debian"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - apt-get update && - apt-get install -y apt-transport-https ca-certificates curl software-properties-common && - curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && - echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && - apt-get update && - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, dockerArch)) - case strings.Contains(osRelease, "ID=fedora"): - // Detect Fedora version to handle DNF 5 changes - versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'") - versionOutput, err := versionCmd.Output() - var fedoraVersion int - if err == nil { - if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil { - fedoraVersion = v - } - } - - // Use appropriate DNF syntax based on version - var repoCmd string - if fedoraVersion >= 41 { - // DNF 5 syntax for Fedora 41+ - repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo" - } else { - // DNF 4 syntax for Fedora < 41 - repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo" - } - - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` - dnf -y install dnf-plugins-core && - %s && - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `, repoCmd)) - case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): - installCmd = exec.Command("bash", "-c", ` - zypper install -y docker docker-compose && - systemctl enable docker - `) - case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): - installCmd = exec.Command("bash", "-c", ` - dnf remove -y runc && - dnf -y install yum-utils && - dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && - systemctl enable docker - `) - case strings.Contains(osRelease, "ID=amzn"): - installCmd = exec.Command("bash", "-c", ` - yum update -y && - yum install -y docker && - systemctl enable docker && - usermod -a -G docker ec2-user - `) - default: - return fmt.Errorf("unsupported Linux distribution") - } - - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - return installCmd.Run() -} - -func startDockerService() error { - if runtime.GOOS == "linux" { - cmd := exec.Command("systemctl", "enable", "--now", "docker") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } else if runtime.GOOS == "darwin" { - // On macOS, Docker is usually started via the Docker Desktop application - fmt.Println("Please start Docker Desktop manually on macOS.") - return nil - } - return fmt.Errorf("unsupported operating system for starting Docker service") -} - -func isDockerInstalled() bool { - return isContainerInstalled("docker") -} - -func isPodmanInstalled() bool { - return isContainerInstalled("podman") && isContainerInstalled("podman-compose") -} - -func isContainerInstalled(container string) bool { - cmd := exec.Command(container, "--version") - if err := cmd.Run(); err != nil { - return false - } - return true -} - -func isUserInDockerGroup() bool { - if runtime.GOOS == "darwin" { - // Docker group is not applicable on macOS - // So we assume that the user can run Docker commands - return true - } - - if os.Geteuid() == 0 { - return true // Root user can run Docker commands anyway - } - - // Check if the current user is in the docker group - if dockerGroup, err := user.LookupGroup("docker"); err == nil { - if currentUser, err := user.Current(); err == nil { - if currentUserGroupIds, err := currentUser.GroupIds(); err == nil { - for _, groupId := range currentUserGroupIds { - if groupId == dockerGroup.Gid { - return true - } - } - } - } - } - - // Eventually, if any of the checks fail, we assume the user cannot run Docker commands - return false -} - -// isDockerRunning checks if the Docker daemon is running by using the `docker info` command. -func isDockerRunning() bool { - cmd := exec.Command("docker", "info") - if err := cmd.Run(); err != nil { - return false - } - return true -} - -// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied -func executeDockerComposeCommandWithArgs(args ...string) error { - var cmd *exec.Cmd - var useNewStyle bool - - if !isDockerInstalled() { - return fmt.Errorf("docker is not installed") - } - - checkCmd := exec.Command("docker", "compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = true - } else { - checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = false - } else { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available") - } - } - - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) - } else { - cmd = exec.Command("docker-compose", args...) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// pullContainers pulls the containers using the appropriate command. -func pullContainers(containerType SupportedContainer) error { - fmt.Println("Pulling the container images...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// startContainers starts the containers using the appropriate command. -func startContainers(containerType SupportedContainer) error { - fmt.Println("Starting containers...") - - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { - return fmt.Errorf("failed start containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { - return fmt.Errorf("failed to start containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// stopContainers stops the containers using the appropriate command. -func stopContainers(containerType SupportedContainer) error { - fmt.Println("Stopping containers...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil { - return fmt.Errorf("failed to stop containers: %v", err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { - return fmt.Errorf("failed to stop containers: %v", err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} - -// restartContainer restarts a specific container using the appropriate command. -func restartContainer(container string, containerType SupportedContainer) error { - fmt.Println("Restarting containers...") - if containerType == Podman { - if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) - } - - return nil - } - - if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) - } - - return nil - } - - return fmt.Errorf("Unsupported container type: %s", containerType) -} diff --git a/install/crowdsec.go b/install/crowdsec.go index 2e388e92..c17bf540 100644 --- a/install/crowdsec.go +++ b/install/crowdsec.go @@ -13,7 +13,7 @@ import ( func installCrowdsec(config Config) error { - if err := stopContainers(config.InstallationContainerType); err != nil { + if err := stopContainers(); err != nil { return fmt.Errorf("failed to stop containers: %v", err) } @@ -72,12 +72,12 @@ func installCrowdsec(config Config) error { os.Exit(1) } - if err := startContainers(config.InstallationContainerType); err != nil { + if err := startContainers(); err != nil { return fmt.Errorf("failed to start containers: %v", err) } // get API key - apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType) + apiKey, err := GetCrowdSecAPIKey() if err != nil { return fmt.Errorf("failed to get API key: %v", err) } @@ -87,7 +87,7 @@ func installCrowdsec(config Config) error { return fmt.Errorf("failed to replace bouncer key: %v", err) } - if err := restartContainer("traefik", config.InstallationContainerType); err != nil { + if err := restartContainer("traefik"); err != nil { return fmt.Errorf("failed to restart containers: %v", err) } @@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool { return bytes.Contains(content, []byte("crowdsec:")) } -func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) { +func GetCrowdSecAPIKey() (string, error) { // First, ensure the container is running - if err := waitForContainer("crowdsec", containerType); err != nil { + if err := waitForContainer("crowdsec"); err != nil { return "", fmt.Errorf("waiting for container: %w", err) } diff --git a/install/get-installer.sh b/install/get-installer.sh deleted file mode 100644 index d7f684ce..00000000 --- a/install/get-installer.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/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 8c6e06e2..1d12aa12 100644 --- a/install/go.mod +++ b/install/go.mod @@ -1,10 +1,10 @@ module installer -go 1.24.0 +go 1.23.0 require ( - golang.org/x/term v0.36.0 + golang.org/x/term v0.28.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.37.0 // indirect +require golang.org/x/sys v0.29.0 // indirect diff --git a/install/go.sum b/install/go.sum index 68e246d1..169165e4 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,7 @@ -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= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 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/input.go b/install/input.go deleted file mode 100644 index cf8fd7a3..00000000 --- a/install/input.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "strings" - "syscall" - - "golang.org/x/term" -) - -func readString(reader *bufio.Reader, prompt string, defaultValue string) string { - if defaultValue != "" { - fmt.Printf("%s (default: %s): ", prompt, defaultValue) - } else { - fmt.Print(prompt + ": ") - } - input, _ := reader.ReadString('\n') - input = strings.TrimSpace(input) - if input == "" { - return defaultValue - } - return input -} - -func readStringNoDefault(reader *bufio.Reader, prompt string) string { - fmt.Print(prompt + ": ") - input, _ := reader.ReadString('\n') - return strings.TrimSpace(input) -} - -func readPassword(prompt string, reader *bufio.Reader) string { - if term.IsTerminal(int(syscall.Stdin)) { - fmt.Print(prompt + ": ") - // Read password without echo if we're in a terminal - password, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() // Add a newline since ReadPassword doesn't add one - if err != nil { - return "" - } - input := strings.TrimSpace(string(password)) - if input == "" { - return readPassword(prompt, reader) - } - return input - } else { - // Fallback to reading from stdin if not in a terminal - return readString(reader, prompt, "") - } -} - -func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { - defaultStr := "no" - if defaultValue { - defaultStr = "yes" - } - input := readString(reader, prompt+" (yes/no)", defaultStr) - return strings.ToLower(input) == "yes" -} - -func readBoolNoDefault(reader *bufio.Reader, prompt string) bool { - input := readStringNoDefault(reader, prompt+" (yes/no)") - return strings.ToLower(input) == "yes" -} - -func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { - input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) - if input == "" { - return defaultValue - } - value := defaultValue - fmt.Sscanf(input, "%d", &value) - return value -} diff --git a/install/input.txt b/install/input.txt index 12df39d7..9bca8081 100644 --- a/install/input.txt +++ b/install/input.txt @@ -1,7 +1,5 @@ -docker example.com pangolin.example.com -yes admin@example.com yes admin@example.com diff --git a/install/main.go b/install/main.go index 72ffbac0..abb67acd 100644 --- a/install/main.go +++ b/install/main.go @@ -2,21 +2,23 @@ package main import ( "bufio" + "bytes" "embed" "fmt" "io" "io/fs" - "math/rand" - "net" - "net/http" - "net/url" "os" "os/exec" "path/filepath" "runtime" "strings" + "syscall" "text/template" "time" + "unicode" + "math/rand" + + "golang.org/x/term" ) // DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD @@ -30,70 +32,46 @@ func loadVersions(config *Config) { var configFiles embed.FS type Config struct { - InstallationContainerType SupportedContainer - PangolinVersion string - GerbilVersion string - BadgerVersion string - BaseDomain string - DashboardDomain string - EnableIPv6 bool - LetsEncryptEmail string - EnableEmail bool - EmailSMTPHost string - EmailSMTPPort int - EmailSMTPUser string - EmailSMTPPass string - EmailNoReply string - InstallGerbil bool - TraefikBouncerKey string - DoCrowdsecInstall bool - EnableGeoblocking bool - Secret string + PangolinVersion string + GerbilVersion string + BadgerVersion string + BaseDomain string + DashboardDomain string + LetsEncryptEmail string + AdminUserEmail string + AdminUserPassword string + DisableSignupWithoutInvite bool + DisableUserCreateOrg bool + EnableEmail bool + EmailSMTPHost string + EmailSMTPPort int + EmailSMTPUser string + EmailSMTPPass string + EmailNoReply string + InstallGerbil bool + TraefikBouncerKey string + DoCrowdsecInstall bool + Secret string } -type SupportedContainer string - -const ( - Docker SupportedContainer = "docker" - Podman SupportedContainer = "podman" - Undefined SupportedContainer = "undefined" -) - func main() { - - // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking - - fmt.Println("Welcome to the Pangolin installer!") - fmt.Println("This installer will help you set up Pangolin on your server.") - fmt.Println("\nPlease make sure you have the following prerequisites:") - fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") - fmt.Println("\nLets get started!") - - if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS - for _, p := range []int{80, 443} { - if err := checkPortsAvailable(p); err != nil { - fmt.Fprintln(os.Stderr, err) - - fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly. If you already have the Pangolin stack running, shut them down before proceeding.\n") - os.Exit(1) - } - } - } - reader := bufio.NewReader(os.Stdin) + // check if the user is root + if os.Geteuid() != 0 { + fmt.Println("This script must be run as root") + os.Exit(1) + } + var config Config - var alreadyInstalled = false + config.DoCrowdsecInstall = false + config.Secret = generateRandomSecretKey() // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { config = collectUserInput(reader) loadVersions(&config) - config.DoCrowdsecInstall = false - config.Secret = generateRandomSecretKey() - - fmt.Println("\n=== Generating Configuration Files ===") if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) @@ -102,89 +80,29 @@ func main() { moveFile("config/docker-compose.yml", "docker-compose.yml") - 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.") + if !isDockerInstalled() && runtime.GOOS == "linux" { + if readBool(reader, "Docker is not installed. Would you like to install it?", true) { + installDocker() } } fmt.Println("\n=== Starting installation ===") - if readBool(reader, "Would you like to install and start the containers?", true) { + if isDockerInstalled() { + if readBool(reader, "Would you like to install and start the containers?", true) { + if err := pullContainers(); err != nil { + fmt.Println("Error: ", err) + return + } - config.InstallationContainerType = podmanOrDocker(reader) - - if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { - if readBool(reader, "Docker is not installed. Would you like to install it?", true) { - installDocker() - // try to start docker service but ignore errors - if err := startDockerService(); err != nil { - fmt.Println("Error starting Docker service:", err) - } else { - fmt.Println("Docker service started successfully!") - } - // wait 10 seconds for docker to start checking if docker is running every 2 seconds - fmt.Println("Waiting for Docker to start...") - for i := 0; i < 5; i++ { - if isDockerRunning() { - fmt.Println("Docker is running!") - break - } - fmt.Println("Docker is not running yet, waiting...") - time.Sleep(2 * time.Second) - } - if !isDockerRunning() { - fmt.Println("Docker is still not running after 10 seconds. Please check the installation.") - os.Exit(1) - } - fmt.Println("Docker installed successfully!") + if err := startContainers(); err != nil { + fmt.Println("Error: ", err) + return } } - - if err := pullContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return - } - - if err := startContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return - } } - } else { - 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\"") - } - } + fmt.Println("Looks like you already installed, so I am going to do the setup...") } if !checkIsCrowdsecInstalledInCompose() { @@ -192,28 +110,14 @@ func main() { // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.") - - // BUG: crowdsec installation will be skipped if the user chooses to install on the first installation. if readBool(reader, "Are you willing to manage CrowdSec?", false) { if config.DashboardDomain == "" { - traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml") + traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") if err != nil { fmt.Printf("Error reading config: %v\n", err) return } - appConfig, err := ReadAppConfig("config/config.yml") - if err != nil { - fmt.Printf("Error reading config: %v\n", err) - return - } - - parsedURL, err := url.Parse(appConfig.DashboardURL) - if err != nil { - fmt.Printf("Error parsing URL: %v\n", err) - return - } - - config.DashboardDomain = parsedURL.Hostname() + config.DashboardDomain = traefikConfig.DashboardDomain config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail config.BadgerVersion = traefikConfig.BadgerVersion @@ -228,110 +132,66 @@ func main() { } } - config.InstallationContainerType = podmanOrDocker(reader) - config.DoCrowdsecInstall = true - err := installCrowdsec(config) - if err != nil { - fmt.Printf("Error installing CrowdSec: %v\n", err) - return - } - - fmt.Println("CrowdSec installed successfully!") - return + installCrowdsec(config) } } } - if !alreadyInstalled { - // Setup Token Section - fmt.Println("\n=== Setup Token ===") - - // Check if containers were started during this installation - containersStarted := false - if (isDockerInstalled() && config.InstallationContainerType == Docker) || - (isPodmanInstalled() && config.InstallationContainerType == Podman) { - // Try to fetch and display the token if containers are running - containersStarted = true - printSetupToken(config.InstallationContainerType, config.DashboardDomain) - } - - // If containers weren't started or token wasn't found, show instructions - if !containersStarted { - showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) - } - } - - fmt.Println("\nInstallation complete!") - - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) + fmt.Println("Installation complete!") } -func podmanOrDocker(reader *bufio.Reader) SupportedContainer { - inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") - - chosenContainer := Docker - if strings.EqualFold(inputContainer, "docker") { - chosenContainer = Docker - } else if strings.EqualFold(inputContainer, "podman") { - chosenContainer = Podman +func readString(reader *bufio.Reader, prompt string, defaultValue string) string { + if defaultValue != "" { + fmt.Printf("%s (default: %s): ", prompt, defaultValue) } else { - fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer) - os.Exit(1) + fmt.Print(prompt + ": ") } + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + if input == "" { + return defaultValue + } + return input +} - if chosenContainer == Podman { - if !isPodmanInstalled() { - fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") - os.Exit(1) +func readPassword(prompt string, reader *bufio.Reader) string { + if term.IsTerminal(int(syscall.Stdin)) { + fmt.Print(prompt + ": ") + // Read password without echo if we're in a terminal + password, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() // Add a newline since ReadPassword doesn't add one + if err != nil { + return "" } - - if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil { - fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") - fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") - approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true) - if approved { - if os.Geteuid() != 0 { - fmt.Println("You need to run the installer as root for such a configuration.") - os.Exit(1) - } - - // Podman containers are not able to listen on privileged ports. The official recommendation is to - // container low-range ports as unprivileged ports. - // Linux only. - - if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil { - fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err) - os.Exit(1) - } - } else { - fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.") - } - } else { - fmt.Println("Unprivileged ports have been configured.") - } - - } else if chosenContainer == Docker { - // check if docker is not installed and the user is root - if !isDockerInstalled() { - if os.Geteuid() != 0 { - fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.") - os.Exit(1) - } - } - - // check if the user is in the docker group (linux only) - if !isUserInDockerGroup() { - fmt.Println("You are not in the docker group.") - fmt.Println("The installer will not be able to run docker commands without running it as root.") - os.Exit(1) + input := strings.TrimSpace(string(password)) + if input == "" { + return readPassword(prompt, reader) } + return input } else { - // This shouldn't happen unless there's a third container runtime. - os.Exit(1) + // Fallback to reading from stdin if not in a terminal + return readString(reader, prompt, "") } +} - return chosenContainer +func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { + defaultStr := "no" + if defaultValue { + defaultStr = "yes" + } + input := readString(reader, prompt+" (yes/no)", defaultStr) + return strings.ToLower(input) == "yes" +} + +func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { + input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) + if input == "" { + return defaultValue + } + value := defaultValue + fmt.Sscanf(input, "%d", &value) + return value } func collectUserInput(reader *bufio.Reader) Config { @@ -339,27 +199,49 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - - // Set default dashboard domain after base domain is collected - defaultDashboardDomain := "" - if config.BaseDomain != "" { - defaultDashboardDomain = "pangolin." + config.BaseDomain - } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain) 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) + // Admin user configuration + fmt.Println("\n=== Admin User Configuration ===") + config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain) + for { + pass1 := readPassword("Create admin user password", reader) + pass2 := readPassword("Confirm admin user password", reader) + + if pass1 != pass2 { + fmt.Println("Passwords do not match") + } else { + config.AdminUserPassword = pass1 + if valid, message := validatePassword(config.AdminUserPassword); valid { + break + } else { + fmt.Println("Invalid password:", message) + fmt.Println("Password requirements:") + fmt.Println("- At least one uppercase English letter") + fmt.Println("- At least one lowercase English letter") + fmt.Println("- At least one digit") + fmt.Println("- At least one special character") + } + } + } + + // Security settings + fmt.Println("\n=== Security Settings ===") + config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true) + config.DisableUserCreateOrg = readBool(reader, "Disable users from creating organizations", false) + // Email configuration fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + config.EnableEmail = readBool(reader, "Enable email functionality", 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.EmailSMTPPass = readString(reader, "Enter SMTP password", "") config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } @@ -368,26 +250,68 @@ func collectUserInput(reader *bufio.Reader) Config { 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 - - 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") os.Exit(1) } + if config.LetsEncryptEmail == "" { + fmt.Println("Error: Let's Encrypt email is required") + os.Exit(1) + } + if config.AdminUserEmail == "" || config.AdminUserPassword == "" { + fmt.Println("Error: Admin user email and password are required") + os.Exit(1) + } return config } +func validatePassword(password string) (bool, string) { + if len(password) == 0 { + return false, "Password cannot be empty" + } + + var ( + hasUpper bool + hasLower bool + hasDigit bool + hasSpecial bool + ) + + for _, char := range password { + switch { + case unicode.IsUpper(char): + hasUpper = true + case unicode.IsLower(char): + hasLower = true + case unicode.IsDigit(char): + hasDigit = true + case unicode.IsPunct(char) || unicode.IsSymbol(char): + hasSpecial = true + } + } + + var missing []string + if !hasUpper { + missing = append(missing, "an uppercase letter") + } + if !hasLower { + missing = append(missing, "a lowercase letter") + } + if !hasDigit { + missing = append(missing, "a digit") + } + if !hasSpecial { + missing = append(missing, "a special character") + } + + if len(missing) > 0 { + return false, fmt.Sprintf("Password must contain %s", strings.Join(missing, ", ")) + } + + return true, "" +} + func createConfigFiles(config Config) error { os.MkdirAll("config", 0755) os.MkdirAll("config/letsencrypt", 0755) @@ -457,6 +381,7 @@ func createConfigFiles(config Config) error { return nil }) + if err != nil { return fmt.Errorf("error walking config files: %v", err) } @@ -464,6 +389,171 @@ func createConfigFiles(config Config) error { return nil } +func installDocker() error { + // Detect Linux distribution + cmd := exec.Command("cat", "/etc/os-release") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to detect Linux distribution: %v", err) + } + osRelease := string(output) + + // Detect system architecture + archCmd := exec.Command("uname", "-m") + archOutput, err := archCmd.Output() + if err != nil { + return fmt.Errorf("failed to detect system architecture: %v", err) + } + arch := strings.TrimSpace(string(archOutput)) + + // Map architecture to Docker's architecture naming + var dockerArch string + switch arch { + case "x86_64": + dockerArch = "amd64" + case "aarch64": + dockerArch = "arm64" + default: + return fmt.Errorf("unsupported architecture: %s", arch) + } + + var installCmd *exec.Cmd + switch { + case strings.Contains(osRelease, "ID=ubuntu"): + installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + apt-get update && + apt-get install -y apt-transport-https ca-certificates curl software-properties-common && + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && + echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && + apt-get update && + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `, dockerArch)) + case strings.Contains(osRelease, "ID=debian"): + installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + apt-get update && + apt-get install -y apt-transport-https ca-certificates curl software-properties-common && + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && + echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && + apt-get update && + apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `, dockerArch)) + case strings.Contains(osRelease, "ID=fedora"): + installCmd = exec.Command("bash", "-c", ` + dnf -y install dnf-plugins-core && + dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + `) + case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): + installCmd = exec.Command("bash", "-c", ` + zypper install -y docker docker-compose && + systemctl enable docker + `) + case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): + installCmd = exec.Command("bash", "-c", ` + dnf remove -y runc && + dnf -y install yum-utils && + dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && + systemctl enable docker + `) + case strings.Contains(osRelease, "ID=amzn"): + installCmd = exec.Command("bash", "-c", ` + yum update -y && + yum install -y docker && + systemctl enable docker && + usermod -a -G docker ec2-user + `) + default: + return fmt.Errorf("unsupported Linux distribution") + } + installCmd.Stdout = os.Stdout + installCmd.Stderr = os.Stderr + return installCmd.Run() +} + +func isDockerInstalled() bool { + cmd := exec.Command("docker", "--version") + if err := cmd.Run(); err != nil { + return false + } + return true +} + +// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied +func executeDockerComposeCommandWithArgs(args ...string) error { + var cmd *exec.Cmd + var useNewStyle bool + + if !isDockerInstalled() { + return fmt.Errorf("docker is not installed") + } + + checkCmd := exec.Command("docker", "compose", "version") + if err := checkCmd.Run(); err == nil { + useNewStyle = true + } else { + checkCmd = exec.Command("docker-compose", "version") + if err := checkCmd.Run(); err == nil { + useNewStyle = false + } else { + return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available") + } + } + + if useNewStyle { + cmd = exec.Command("docker", append([]string{"compose"}, args...)...) + } else { + cmd = exec.Command("docker-compose", args...) + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// pullContainers pulls the containers using the appropriate command. +func pullContainers() error { + fmt.Println("Pulling the container images...") + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) + } + + return nil +} + +// startContainers starts the containers using the appropriate command. +func startContainers() error { + fmt.Println("Starting containers...") + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { + return fmt.Errorf("failed to start containers: %v", err) + } + + return nil +} + +// stopContainers stops the containers using the appropriate command. +func stopContainers() error { + fmt.Println("Stopping containers...") + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { + return fmt.Errorf("failed to stop containers: %v", err) + } + + return nil +} + +// restartContainer restarts a specific container using the appropriate command. +func restartContainer(container string) error { + fmt.Println("Restarting containers...") + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } + + return nil +} + func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil { @@ -489,91 +579,32 @@ func moveFile(src, dst string) error { return os.Remove(src) } -func printSetupToken(containerType SupportedContainer, dashboardDomain string) { - fmt.Println("Waiting for Pangolin to generate setup token...") +func waitForContainer(containerName string) error { + maxAttempts := 30 + retryInterval := time.Second * 2 - // Wait for Pangolin to be healthy - if err := waitForContainer("pangolin", containerType); err != nil { - fmt.Println("Warning: Pangolin container did not become healthy in time.") - return - } + for attempt := 0; attempt < maxAttempts; attempt++ { + // Check if container is running + cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName) + var out bytes.Buffer + cmd.Stdout = &out - // Give a moment for the setup token to be generated - time.Sleep(2 * time.Second) - - // Fetch logs - var cmd *exec.Cmd - if containerType == Docker { - cmd = exec.Command("docker", "logs", "pangolin") - } else { - cmd = exec.Command("podman", "logs", "pangolin") - } - output, err := cmd.Output() - if err != nil { - fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.") - return - } - - // Parse for setup token - lines := strings.Split(string(output), "\n") - for i, line := range lines { - if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") { - // Look for "Token: ..." in the next few lines - for j := i + 1; j < i+5 && j < len(lines); j++ { - trimmedLine := strings.TrimSpace(lines[j]) - if strings.Contains(trimmedLine, "Token:") { - // Extract token after "Token:" - tokenStart := strings.Index(trimmedLine, "Token:") - if tokenStart != -1 { - token := strings.TrimSpace(trimmedLine[tokenStart+6:]) - fmt.Printf("Setup token: %s\n", token) - fmt.Println("") - fmt.Println("This token is required to register the first admin account in the web UI at:") - fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain) - fmt.Println("") - fmt.Println("Save this token securely. It will be invalid after the first admin is created.") - return - } - } - } + if err := cmd.Run(); err != nil { + // If the container doesn't exist or there's another error, wait and retry + time.Sleep(retryInterval) + continue } - } - fmt.Println("Warning: Could not find a setup token in Pangolin logs.") -} -func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) { - fmt.Println("\n=== Setup Token Instructions ===") - fmt.Println("To get your setup token, you need to:") - fmt.Println("") - fmt.Println("1. Start the containers") - if containerType == Docker { - fmt.Println(" docker compose up -d") - } else if containerType == Podman { - fmt.Println(" podman-compose up -d") - } else { + isRunning := strings.TrimSpace(out.String()) == "true" + if isRunning { + return nil + } + + // Container exists but isn't running yet, wait and retry + time.Sleep(retryInterval) } - fmt.Println("") - fmt.Println("2. Wait for the Pangolin container to start and generate the token") - fmt.Println("") - fmt.Println("3. Check the container logs for the setup token") - if containerType == Docker { - fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") - } else 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(" === SETUP TOKEN GENERATED ===") - fmt.Println(" Token: [your-token-here]") - fmt.Println(" Use this token on the initial setup page") - fmt.Println("") - fmt.Println("5. Use the token to complete initial setup at") - fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain) - fmt.Println("") - fmt.Println("The setup token is required to register the first admin account.") - fmt.Println("Save it securely - it will be invalid after the first admin is created.") - fmt.Println("================================") + + return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) } func generateRandomSecretKey() string { @@ -588,84 +619,4 @@ func generateRandomSecretKey() string { b[i] = charset[seededRand.Intn(len(charset))] } return string(b) -} - -func getPublicIP() string { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get("https://ifconfig.io/ip") - if err != nil { - return "" - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "" - } - - ip := strings.TrimSpace(string(body)) - - // Validate that it's a valid IP address - if net.ParseIP(ip) != nil { - return ip - } - - return "" -} - -// Run external commands with stdio/stderr attached. -func run(name string, args ...string) error { - cmd := exec.Command(name, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -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 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 -} +} \ No newline at end of file diff --git a/internationalization/de.md b/internationalization/de.md new file mode 100644 index 00000000..c84249f7 --- /dev/null +++ b/internationalization/de.md @@ -0,0 +1,287 @@ +## Authentication Site + +| EN | DE | Notes | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Bereitgestellt von [Pangolin](https://github.com/fosrl/pangolin) | | +| Authentication Required | Authentifizierung erforderlich | | +| Choose your preferred method to access {resource} | Wählen Sie Ihre bevorzugte Methode, um auf {resource} zuzugreifen | | +| PIN | PIN | | +| User | Benutzer | | +| 6-digit PIN Code | 6-stelliger PIN-Code | pin login | +| Login in with PIN | Mit PIN anmelden | pin login | +| Email | E-Mail | user login | +| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | user login | +| Password | Passwort | user login | +| Enter your password | Geben Sie Ihr Passwort ein | user login | +| Forgot your password? | Passwort vergessen? | user login | +| Log in | Anmelden | user login | + +--- + +## Login site + +| EN | DE | Notes | +| --------------------- | ---------------------------------- | ----------- | +| Welcome to Pangolin | Willkommen bei Pangolin | | +| Log in to get started | Melden Sie sich an, um zu beginnen | | +| Email | E-Mail | | +| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | placeholder | +| Password | Passwort | | +| Enter your password | Geben Sie Ihr Passwort ein | placeholder | +| Forgot your password? | Passwort vergessen? | | +| Log in | Anmelden | | + +# Ogranization site after successful login + +| EN | DE | Notes | +| ----------------------------------------- | -------------------------------------------- | ----- | +| Welcome to Pangolin | Willkommen bei Pangolin | | +| You're a member of {number} organization. | Sie sind Mitglied von {number} Organisation. | | + +## Shared Header, Navbar and Footer +##### Header + +| EN | DE | Notes | +| ------------------- | ------------------- | ----- | +| Documentation | Dokumentation | | +| Support | Support | | +| Organization {name} | Organisation {name} | | +##### Organization selector + +| EN | DE | Notes | +| ---------------- | ----------------- | ----- | +| Search… | Suchen… | | +| Create | Erstellen | | +| New Organization | Neue Organisation | | +| Organizations | Organisationen | | + +##### Navbar + +| EN | DE | Notes | +| --------------- | ----------------- | ----- | +| Sites | Websites | | +| Resources | Ressourcen | | +| User & Roles | Benutzer & Rollen | | +| Shareable Links | Teilbare Links | | +| General | Allgemein | | +##### Footer +| EN | DE | | +| ------------------------- | --------------------------- | ------------------- | +| Page {number} of {number} | Seite {number} von {number} | | +| Rows per page | Zeilen pro Seite | | +| Pangolin | Pangolin | unten auf der Seite | +| Built by Fossorial | Erstellt von Fossorial | unten auf der Seite | +| Open Source | Open Source | unten auf der Seite | +| Documentation | Dokumentation | unten auf der Seite | +| {version} | {version} | unten auf der Seite | + +## Main “Sites” +##### “Hero” section + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Newt (Recommended) | Newt (empfohlen) | | +| 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. | Für das beste Benutzererlebnis verwenden Sie Newt. Es nutzt WireGuard im Hintergrund und ermöglicht es Ihnen, auf Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk direkt aus dem Pangolin-Dashboard zuzugreifen. | | +| Runs in Docker | Läuft in Docker | | +| Runs in shell on macOS, Linux, and Windows | Läuft in der Shell auf macOS, Linux und Windows | | +| Install Newt | Newt installieren | | +| Basic WireGuard
| Verwenden Sie einen beliebigen WireGuard-Client, um eine Verbindung herzustellen. Sie müssen auf Ihre internen Ressourcen über die Peer-IP-Adresse zugreifen. | | +| Compatible with all WireGuard clients
| Kompatibel mit allen WireGuard-Clients
| | +| Manual configuration required | Manuelle Konfiguration erforderlich
| | +##### Content + +| EN | DE | Notes | +| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- | +| Manage Sites | Seiten verwalten | | +| Allow connectivity to your network through secure tunnels | Ermöglichen Sie die Verbindung zu Ihrem Netzwerk über ein sicheren Tunnel | | +| Search sites | Seiten suchen | placeholder | +| Add Site | Seite hinzufügen | | +| Name | Name | table header | +| Online | Status | table header | +| Site | Seite | table header | +| Data In | Eingehende Daten | table header | +| Data Out | Ausgehende Daten | table header | +| Connection Type | Verbindungstyp | table header | +| Online | Online | site state | +| Offline | Offline | site state | +| Edit → | Bearbeiten → | | +| View settings | Einstellungen anzeigen | Popup after clicking “…” on site | +| Delete | Löschen | Popup after clicking “…” on site | +##### Add Site Popup + +| EN | DE | Notes | +| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- | +| Create Site | Seite erstellen | | +| Create a new site to start connection for this site | Erstellen Sie eine neue Seite, um die Verbindung zu starten | | +| Name | Name | | +| Site name | Seiten-Name | placeholder | +| This is the name that will be displayed for this site. | So wird Ihre Seite angezeigt | desc | +| Method | Methode | | +| Local | Lokal | | +| Newt | Newt | | +| WireGuard | WireGuard | | +| This is how you will expose connections. | So werden Verbindungen freigegeben. | | +| You will only be able to see the configuration once. | Diese Konfiguration können Sie nur einmal sehen. | | +| Learn how to install Newt on your system | Erfahren Sie, wie Sie Newt auf Ihrem System installieren | | +| I have copied the config | Ich habe die Konfiguration kopiert | | +| Create Site | Website erstellen | | +| Close | Schließen | | + +## Main “Resources” + +##### “Hero” section + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Resources | Ressourcen | | +| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | | +| Secure connectivity with WireGuard encryption | Sichere Verbindung mit WireGuard-Verschlüsselung | | +| Configure multiple authentication methods | Konfigurieren Sie mehrere Authentifizierungsmethoden | | +| User and role-based access control | Benutzer- und rollenbasierte Zugriffskontrolle | | +##### Content + +| EN | DE | Notes | +| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- | +| Manage Resources | Ressourcen verwalten | | +| Create secure proxies to your private applications | Erstellen Sie sichere Proxys für Ihre privaten Anwendungen | | +| Search resources | Ressourcen durchsuchen | placeholder | +| Name | Name | | +| Site | Website | | +| Full URL | Vollständige URL | | +| Authentication | Authentifizierung | | +| Not Protected | Nicht geschützt | authentication state | +| Protected | Geschützt | authentication state | +| Edit → | Bearbeiten → | | +| Add Resource | Ressource hinzufügen | | +##### Add Resource Popup + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- | +| Create Resource | Ressource erstellen | | +| Create a new resource to proxy request to your app | Erstellen Sie eine neue Ressource, um Anfragen an Ihre App zu proxen | | +| Name | Name | | +| My Resource | Neue Ressource | name placeholder | +| This is the name that will be displayed for this resource. | Dies ist der Name, der für diese Ressource angezeigt wird | | +| Subdomain | Subdomain | | +| Enter subdomain | Subdomain eingeben | | +| This is the fully qualified domain name that will be used to access the resource. | Dies ist der vollständige Domainname, der für den Zugriff auf die Ressource verwendet wird. | | +| Site | Website | | +| Search site… | Website suchen… | Site selector popup | +| This is the site that will be used in the dashboard. | Dies ist die Website, die im Dashboard verwendet wird. | | +| Create Resource | Ressource erstellen | | +| Close | Schließen | | + + +## Main “User & Roles” +##### Content + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- | +| Manage User & Roles | Benutzer & Rollen verwalten | | +| Invite users and add them to roles to manage access to your organization | Laden Sie Benutzer ein und weisen Sie ihnen Rollen zu, um den Zugriff auf Ihre Organisation zu verwalten | | +| Users | Benutzer | sidebar item | +| Roles | Rollen | sidebar item | +| **User tab** | | | +| Search users | Benutzer suchen | placeholder | +| Invite User | Benutzer einladen | addbutton | +| Email | E-Mail | table header | +| Status | Status | table header | +| Role | Rolle | table header | +| Confirmed | Bestätigt | account status | +| Not confirmed (?) | Nicht bestätigt (?) | unknown for me account status | +| Owner | Besitzer | role | +| Admin | Administrator | role | +| Member | Mitglied | role | +| **Roles Tab** | | | +| Search roles | Rollen suchen | placeholder | +| Add Role | Rolle hinzufügen | addbutton | +| Name | Name | table header | +| Description | Beschreibung | table header | +| Admin | Administrator | role | +| Member | Mitglied | role | +| Admin role with the most permissions | Administratorrolle mit den meisten Berechtigungen | admin role desc | +| Members can only view resources | Mitglieder können nur Ressourcen anzeigen | member role desc | + +##### Invite User popup + +| EN | DE | Notes | +| ----------------- | ------------------------------------------------------- | ----------- | +| Invite User | Geben Sie neuen Benutzern Zugriff auf Ihre Organisation | | +| Email | E-Mail | | +| Enter an email | E-Mail eingeben | placeholder | +| Role | Rolle | | +| Select role | Rolle auswählen | placeholder | +| Gültig für | Gültig bis | | +| 1 day | Tag | | +| 2 days | 2 Tage | | +| 3 days | 3 Tage | | +| 4 days | 4 Tage | | +| 5 days | 5 Tage | | +| 6 days | 6 Tage | | +| 7 days | 7 Tage | | +| Create Invitation | Einladung erstellen | | +| Close | Schließen | | + + +## Main “Shareable Links” +##### “Hero” section + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Shareable Links | Teilbare Links | | +| 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. | Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Gültigkeitsdauer des Links beim Erstellen konfigurieren. | | +| Easy to create and share | Einfach zu erstellen und zu teilen | | +| Configurable expiration duration | Konfigurierbare Gültigkeitsdauer | | +| Secure and revocable | Sicher und widerrufbar | | +##### Content + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- | +| Manage Shareable Links | Teilbare Links verwalten | | +| Create shareable links to grant temporary or permanent access to your resources | Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren | | +| Search links | Links suchen | placeholder | +| Create Share Link | Neuen Link erstellen | addbutton | +| Resource | Ressource | table header | +| Title | Titel | table header | +| Created | Erstellt | table header | +| Expires | Gültig bis | table header | +| No links. Create one to get started. | Keine Links. Erstellen Sie einen, um zu beginnen. | table placeholder | + +##### Create Shareable Link popup + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- | +| Create Shareable Link | Teilbaren Link erstellen | | +| Anyone with this link can access the resource | Jeder mit diesem Link kann auf die Ressource zugreifen | | +| Resource | Ressource | | +| Select resource | Ressource auswählen | | +| Search resources… | Ressourcen suchen… | resource selector popup | +| Title (optional) | Titel (optional) | | +| Enter title | Titel eingeben | placeholder | +| Expire in | Gültig bis | | +| Minutes | Minuten | | +| Hours | Stunden | | +| Days | Tage | | +| Months | Monate | | +| Years | Jahre | | +| Never expire | Nie ablaufen | | +| 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. | Die Gültigkeitsdauer bestimmt, wie lange der Link nutzbar ist und Zugriff auf die Ressource bietet. Nach Ablauf dieser Zeit funktioniert der Link nicht mehr, und Benutzer, die diesen Link verwendet haben, verlieren den Zugriff auf die Ressource. | | +| Create Link | Link erstellen | | +| Close | Schließen | | + + +## Main “General” + +| EN | DE | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ | +| General | Allgemein | | +| Configure your organization’s general settings | Konfigurieren Sie die allgemeinen Einstellungen Ihrer Organisation | | +| General | Allgemein | sidebar item | +| Organization Settings | Organisationseinstellungen | | +| Manage your organization details and configuration | Verwalten Sie die Details und Konfiguration Ihrer Organisation | | +| Name | Name | | +| This is the display name of the org | Dies ist der Anzeigename Ihrer Organisation | | +| Save Settings | Einstellungen speichern | | +| Danger Zone | Gefahrenzone | | +| Once you delete this org, there is no going back. Please be certain. | Wenn Sie diese Organisation löschen, gibt es kein Zurück. Bitte seien Sie sicher. | | +| Delete Organization Data | Organisationsdaten löschen | | \ No newline at end of file diff --git a/internationalization/es.md b/internationalization/es.md new file mode 100644 index 00000000..c4477fbf --- /dev/null +++ b/internationalization/es.md @@ -0,0 +1,291 @@ +## Authentication Site + + +| EN | ES | Notes | +| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Desarrollado por [Pangolin](https://github.com/fosrl/pangolin) | | +| Authentication Required | Se requiere autenticación | | +| Choose your preferred method to access {resource} | Elije tu método requerido para acceder a {resource} | | +| PIN | PIN | | +| User | Usuario | | +| 6-digit PIN Code | Código PIN de 6 dígitos | pin login | +| Login in with PIN | Registrate con PIN | pin login | +| Email | Email | user login | +| Enter your email | Introduce tu email | user login | +| Password | Contraseña | user login | +| Enter your password | Introduce tu contraseña | user login | +| Forgot your password? | ¿Olvidaste tu contraseña? | user login | +| Log in | Iniciar sesión | user login | + + +## Login site + +| EN | ES | Notes | +| --------------------- | ---------------------------------- | ----------- | +| Welcome to Pangolin | Binvenido a Pangolin | | +| Log in to get started | Registrate para comenzar | | +| Email | Email | | +| Enter your email | Introduce tu email | placeholder | +| Password | Contraseña | | +| Enter your password | Introduce tu contraseña | placeholder | +| Forgot your password? | ¿Olvidaste tu contraseña? | | +| Log in | Iniciar sesión | | + +# Ogranization site after successful login + +| EN | ES | Notes | +| ----------------------------------------- | -------------------------------------------- | ----- | +| Welcome to Pangolin | Binvenido a Pangolin | | +| You're a member of {number} organization. | Eres miembro de la organización {number}. | | + +## Shared Header, Navbar and Footer +##### Header + +| EN | ES | Notes | +| ------------------- | ------------------- | ----- | +| Documentation | Documentación | | +| Support | Soporte | | +| Organization {name} | Organización {name} | | +##### Organization selector + +| EN | ES | Notes | +| ---------------- | ----------------- | ----- | +| Search… | Buscar… | | +| Create | Crear | | +| New Organization | Nueva Organización| | +| Organizations | Organizaciones | | + +##### Navbar + +| EN | ES | Notes | +| --------------- | -----------------------| ----- | +| Sites | Sitios | | +| Resources | Recursos | | +| User & Roles | Usuarios y roles | | +| Shareable Links | Enlaces para compartir | | +| General | General | | + +##### Footer +| EN | ES | | +| ------------------------- | --------------------------- | -------| +| Page {number} of {number} | Página {number} de {number} | footer | +| Rows per page | Filas por página | footer | +| Pangolin | Pangolin | footer | +| Built by Fossorial | Construido por Fossorial | footer | +| Open Source | Código abierto | footer | +| Documentation | Documentación | footer | +| {version} | {version} | footer | + +## Main “Sites” +##### “Hero” section + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Newt (Recommended) | Newt (Recomendado) | | +| 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. | Para obtener la mejor experiencia de usuario, utiliza Newt. Utiliza WireGuard internamente y te permite abordar tus recursos privados mediante tu dirección LAN en tu red privada desde el panel de Pangolin. | | +| Runs in Docker | Se ejecuta en Docker | | +| Runs in shell on macOS, Linux, and Windows | Se ejecuta en shell en macOS, Linux y Windows | | +| Install Newt | Instalar Newt | | +| Basic WireGuard
| WireGuard básico
| | +| Compatible with all WireGuard clients
| Compatible con todos los clientes WireGuard
| | +| Manual configuration required | Se requiere configuración manual | | + +##### Content + +| EN | ES | Notes | +| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- | +| Manage Sites | Administrar sitios | | +| Allow connectivity to your network through secure tunnels | Permitir la conectividad a tu red a través de túneles seguros| | +| Search sites | Buscar sitios | placeholder | +| Add Site | Agregar sitio | | +| Name | Nombre | table header | +| Online | Conectado | table header | +| Site | Sitio | table header | +| Data In | Datos en | table header | +| Data Out | Datos de salida | table header | +| Connection Type | Tipo de conexión | table header | +| Online | Conectado | site state | +| Offline | Desconectado | site state | +| Edit → | Editar → | | +| View settings | Ver configuración | Popup after clicking “…” on site | +| Delete | Borrar | Popup after clicking “…” on site | + +##### Add Site Popup + +| EN | ES | Notes | +| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- | +| Create Site | Crear sitio | | +| Create a new site to start connection for this site | Crear un nuevo sitio para iniciar la conexión para este sitio | | +| Name | Nombre | | +| Site name | Nombre del sitio | placeholder | +| This is the name that will be displayed for this site. | Este es el nombre que se mostrará para este sitio. | desc | +| Method | Método | | +| Local | Local | | +| Newt | Newt | | +| WireGuard | WireGuard | | +| This is how you will expose connections. | Así es como expondrás las conexiones. | | +| You will only be able to see the configuration once. | Solo podrás ver la configuración una vez. | | +| Learn how to install Newt on your system | Aprende a instalar Newt en tu sistema | | +| I have copied the config | He copiado la configuración | | +| Create Site | Crear sitio | | +| Close | Cerrar | | + +## Main “Resources” + +##### “Hero” section + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Resources | Recursos | | +| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. |Los recursos son servidores proxy para aplicaciones que se ejecutan en su red privada. Cree un recurso para cada aplicación HTTP o HTTPS en su red privada. Cada recurso debe estar conectado a un sitio web para proporcionar una conexión privada y segura a través del túnel cifrado WireGuard. | | +| Secure connectivity with WireGuard encryption | Conectividad segura con encriptación WireGuard | | +| Configure multiple authentication methods | Configura múltiples métodos de autenticación | | +| User and role-based access control | Control de acceso basado en usuarios y roles | | + +##### Content + +| EN | ES | Notes | +| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- | +| Manage Resources | Administrar recursos | | +| Create secure proxies to your private applications | Crea servidores proxy seguros para tus aplicaciones privadas | | +| Search resources | Buscar recursos | placeholder | +| Name | Nombre | | +| Site | Sitio | | +| Full URL | URL completa | | +| Authentication | Autenticación | | +| Not Protected | No protegido | authentication state | +| Protected | Protegido | authentication state | +| Edit → | Editar → | | +| Add Resource | Agregar recurso | | + +##### Add Resource Popup + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- | +| Create Resource | Crear recurso | | +| Create a new resource to proxy request to your app | Crea un nuevo recurso para enviar solicitudes a tu aplicación | | +| Name | Nombre | | +| My Resource | Mi recurso | name placeholder | +| This is the name that will be displayed for this resource. | Este es el nombre que se mostrará para este recurso. | | +| Subdomain | Subdominio | | +| Enter subdomain | Ingresar subdominio | | +| This is the fully qualified domain name that will be used to access the resource. | Este es el nombre de dominio completo que se utilizará para acceder al recurso. | | +| Site | Sitio | | +| Search site… | Buscar sitio… | Site selector popup | +| This is the site that will be used in the dashboard. | Este es el sitio que se utilizará en el panel de control. | | +| Create Resource | Crear recurso | | +| Close | Cerrar | | + +## Main “User & Roles” +##### Content + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- | +| Manage User & Roles | Administrar usuarios y roles | | +| Invite users and add them to roles to manage access to your organization | Invita a usuarios y agrégalos a roles para administrar el acceso a tu organización | | +| Users | Usuarios | sidebar item | +| Roles | Roles | sidebar item | +| **User tab** | **Pestaña de usuario** | | +| Search users | Buscar usuarios | placeholder | +| Invite User | Invitar usuario | addbutton | +| Email | Email | table header | +| Status | Estado | table header | +| Role | Role | table header | +| Confirmed | Confirmado | account status | +| Not confirmed (?) | No confirmado (?) | unknown for me account status | +| Owner | Dueño | role | +| Admin | Administrador | role | +| Member | Miembro | role | +| **Roles Tab** | **Pestaña Roles** | | +| Search roles | Buscar roles | placeholder | +| Add Role | Agregar rol | addbutton | +| Name | Nombre | table header | +| Description | Descripción | table header | +| Admin | Administrador | role | +| Member | Miembro | role | +| Admin role with the most permissions | Rol de administrador con más permisos | admin role desc | +| Members can only view resources | Los miembros sólo pueden ver los recursos | member role desc | + +##### Invite User popup + +| EN | ES | Notes | +| ----------------- | ------------------------------------------------------- | ----------- | +| Invite User | Invitar usuario | | +| Email | Email | | +| Enter an email | Introduzca un email | placeholder | +| Role | Rol | | +| Select role | Seleccionar rol | placeholder | +| Gültig für | Válido para | | +| 1 day | 1 día | | +| 2 days | 2 días | | +| 3 days | 3 días | | +| 4 days | 4 días | | +| 5 days | 5 días | | +| 6 days | 6 días | | +| 7 days | 7 días | | +| Create Invitation | Crear invitación | | +| Close | Cerrar | | + + +## Main “Shareable Links” +##### “Hero” section + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- | +| Shareable Links | Enlaces para compartir | | +| 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. | Crear enlaces que se puedan compartir a tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puedes configurar la duración de caducidad del enlace cuando lo creas. | | +| Easy to create and share | Fácil de crear y compartir | | +| Configurable expiration duration | Duración de expiración configurable | | +| Secure and revocable | Seguro y revocable | | +##### Content + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- | +| Manage Shareable Links | Administrar enlaces compartibles | | +| Create shareable links to grant temporary or permanent access to your resources | Crear enlaces compartibles para otorgar acceso temporal o permanente a tus recursos | | +| Search links | Buscar enlaces | placeholder | +| Create Share Link | Crear enlace para compartir | addbutton | +| Resource | Recurso | table header | +| Title | Título | table header | +| Created | Creado | table header | +| Expires | Caduca | table header | +| No links. Create one to get started. | No hay enlaces. Crea uno para comenzar. | table placeholder | + +##### Create Shareable Link popup + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- | +| Create Shareable Link | Crear un enlace para compartir | | +| Anyone with this link can access the resource | Cualquier persona con este enlace puede acceder al recurso. | | +| Resource | Recurso | | +| Select resource | Seleccionar recurso | | +| Search resources… | Buscar recursos… | resource selector popup | +| Title (optional) | Título (opcional) | | +| Enter title | Introducir título | placeholder | +| Expire in | Caduca en | | +| Minutes | Minutos | | +| Hours | Horas | | +| Days | Días | | +| Months | Meses | | +| Years | Años | | +| Never expire | Nunca caduca | | +| 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. | El tiempo de expiración es el tiempo durante el cual el enlace se podrá utilizar y brindará acceso al recurso. Después de este tiempo, el enlace dejará de funcionar y los usuarios que lo hayan utilizado perderán el acceso al recurso. | | +| Create Link | Crear enlace | | +| Close | Cerrar | | + + +## Main “General” + +| EN | ES | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ | +| General | General | | +| Configure your organization’s general settings | Configura los ajustes generales de tu organización | | +| General | General | sidebar item | +| Organization Settings | Configuración de la organización | | +| Manage your organization details and configuration | Administra los detalles y la configuración de tu organización| | +| Name | Nombre | | +| This is the display name of the org | Este es el nombre para mostrar de la organización. | | +| Save Settings | Guardar configuración | | +| Danger Zone | Zona de peligro | | +| Once you delete this org, there is no going back. Please be certain. | Una vez que elimines esta organización, no habrá vuelta atrás. Asegúrate de hacerlo. | | +| Delete Organization Data | Eliminar datos de la organización | | \ No newline at end of file diff --git a/internationalization/pl.md b/internationalization/pl.md new file mode 100644 index 00000000..a55866e2 --- /dev/null +++ b/internationalization/pl.md @@ -0,0 +1,287 @@ +## Authentication Site + + +| EN | PL | Notes | +| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Zasilane przez [Pangolin](https://github.com/fosrl/pangolin) | | +| Authentication Required | Wymagane uwierzytelnienie | | +| Choose your preferred method to access {resource} | Wybierz preferowaną metodę dostępu do {resource} | | +| PIN | PIN | | +| User | Zaloguj | | +| 6-digit PIN Code | 6-cyfrowy kod PIN | pin login | +| Login in with PIN | Zaloguj się PIN’em | pin login | +| Email | Email | user login | +| Enter your email | Wprowadź swój email | user login | +| Password | Hasło | user login | +| Enter your password | Wprowadź swoje hasło | user login | +| Forgot your password? | Zapomniałeś hasła? | user login | +| Log in | Zaloguj | user login | + + +## Login site + +| EN | PL | Notes | +| --------------------- | ------------------------------ | ----------- | +| Welcome to Pangolin | Witaj w Pangolin | | +| Log in to get started | Zaloguj się, aby rozpocząć
| | +| Email | Email | | +| Enter your email | Wprowadź swój adres e-mail
| placeholder | +| Password | Hasło | | +| Enter your password | Wprowadź swoje hasło | placeholder | +| Forgot your password? | Nie pamiętasz hasła? | | +| Log in | Zaloguj | | + +# Ogranization site after successful login + + +| EN | PL | Notes | +| ----------------------------------------- | ------------------------------------------ | ----- | +| Welcome to Pangolin | Witaj w Pangolin | | +| You're a member of {number} organization. | Jesteś użytkownikiem {number} organizacji. | | + +## Shared Header, Navbar and Footer +##### Header + +| EN | PL | Notes | +| ------------------- | ------------------ | ----- | +| Documentation | Dokumentacja | | +| Support | Wsparcie | | +| Organization {name} | Organizacja {name} | | +##### Organization selector + +| EN | PL | Notes | +| ---------------- | ---------------- | ----- | +| Search… | Szukaj… | | +| Create | Utwórz | | +| New Organization | Nowa organizacja | | +| Organizations | Organizacje | | + +##### Navbar + +| EN | PL | Notes | +| --------------- | ---------------------- | ----- | +| Sites | Witryny | | +| Resources | Zasoby | | +| User & Roles | Użytkownicy i Role | | +| Shareable Links | Łącza do udostępniania | | +| General | Ogólne | | +##### Footer +| EN | PL | | +| ------------------------- | -------------------------- | -------------- | +| Page {number} of {number} | Strona {number} z {number} | | +| Rows per page | Wierszy na stronę | | +| Pangolin | Pangolin | bottom of site | +| Built by Fossorial | Stworzone przez Fossorial | bottom of site | +| Open Source | Open source | bottom of site | +| Documentation | Dokumentacja | bottom of site | +| {version} | {version} | bottom of site | +## Main “Sites” +##### “Hero” section + +| EN | PL | Notes | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| Newt (Recommended) | Newt (zalecane) | | +| 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. | Aby zapewnić najlepsze doświadczenie użytkownika, korzystaj z Newt. Wykorzystuje on technologię WireGuard w tle i pozwala na dostęp do Twoich prywatnych zasobów za pomocą ich adresu LAN w prywatnej sieci bezpośrednio z poziomu pulpitu nawigacyjnego Pangolin. | | +| Runs in Docker | Działa w Dockerze | | +| Runs in shell on macOS, Linux, and Windows | Działa w powłoce na systemach macOS, Linux i Windows | | +| Install Newt | Zainstaluj Newt | | +| Podstawowy WireGuard
| Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał uzyskiwać dostęp do swoich wewnętrznych zasobów za pomocą adresu IP równorzędnego | | +| Compatible with all WireGuard clients
| Kompatybilny ze wszystkimi klientami WireGuard
| | +| Manual configuration required | Wymagana ręczna konfiguracja
| | +##### Content + +| EN | PL | Notes | +| --------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------- | +| Manage Sites | Zarządzanie witrynami | | +| Allow connectivity to your network through secure tunnels | Zezwalaj na łączność z Twoją siecią za pośrednictwem bezpiecznych tuneli | | +| Search sites | Szukaj witryny | placeholder | +| Add Site | Dodaj witrynę | | +| Name | Nazwa | table header | +| Online | Status | table header | +| Site | Witryna | table header | +| Data In | Dane wchodzące | table header | +| Data Out | Dane wychodzące | table header | +| Connection Type | Typ połączenia | table header | +| Online | Online | site state | +| Offline | Poza siecią | site state | +| Edit → | Edytuj → | | +| View settings | Pokaż ustawienia | Popup after clicking “…” on site | +| Delete | Usuń | Popup after clicking “…” on site | +##### Add Site Popup + +| EN | PL | Notes | +| ------------------------------------------------------ | --------------------------------------------------- | ----------- | +| Create Site | Utwórz witrynę | | +| Create a new site to start connection for this site | Utwórz nową witrynę aby rozpocząć połączenie | | +| Name | Nazwa | | +| Site name | Nazwa witryny | placeholder | +| This is the name that will be displayed for this site. | Tak będzie wyświetlana twoja witryna | desc | +| Method | Metoda | | +| Local | Lokalna | | +| Newt | Newt | | +| WireGuard | WireGuard | | +| This is how you will expose connections. | Tak będą eksponowane połączenie. | | +| You will only be able to see the configuration once. | Tą konfigurację możesz zobaczyć tylko raz. | | +| Learn how to install Newt on your system | Dowiedz się jak zainstalować Newt na twoim systemie | | +| I have copied the config | Skopiowałem konfigurację | | +| Create Site | Utwórz witrynę | | +| Close | Zamknij | | + +## Main “Resources” + +##### “Hero” section + +| EN | PL | Notes | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| Resources | Zasoby | | +| Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | | +| Secure connectivity with WireGuard encryption | Bezpieczna łączność z szyfrowaniem WireGuard | | +| Configure multiple authentication methods | Konfigurowanie wielu metod uwierzytelniania | | +| User and role-based access control | Kontrola dostępu oparta na użytkownikach i rolach | | +##### Content + +| EN | PL | Notes | +| -------------------------------------------------- | -------------------------------------------------------------- | -------------------- | +| Manage Resources | Zarządzaj zasobami | | +| Create secure proxies to your private applications | Twórz bezpieczne serwery proxy dla swoich prywatnych aplikacji | | +| Search resources | Szukaj w zasobach | placeholder | +| Name | Nazwa | | +| Site | Witryna | | +| Full URL | Pełny URL | | +| Authentication | Uwierzytelnianie | | +| Not Protected | Niezabezpieczony | authentication state | +| Protected | Zabezpieczony | authentication state | +| Edit → | Edytuj → | | +| Add Resource | Dodaj zasób | | +##### Add Resource Popup + +| EN | PL | Notes | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------- | +| Create Resource | Utwórz zasób | | +| Create a new resource to proxy request to your app | Utwórz nowy zasób, aby przekazywać żądania do swojej aplikacji | | +| Name | Nazwa | | +| My Resource | Nowy zasób | name placeholder | +| This is the name that will be displayed for this resource. | To jest nazwa, która będzie wyświetlana dla tego zasobu | | +| Subdomain | Subdomena | | +| Enter subdomain | Wprowadź subdomenę | | +| This is the fully qualified domain name that will be used to access the resource. | To jest pełna nazwa domeny, która będzie używana do dostępu do zasobu. | | +| Site | Witryna | | +| Search site… | Szukaj witryny… | Site selector popup | +| This is the site that will be used in the dashboard. | To jest witryna, która będzie używana w pulpicie nawigacyjnym. | | +| Create Resource | Utwórz zasób | | +| Close | Zamknij | | + + +## Main “User & Roles” +##### Content + +| EN | PL | Notes | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------- | +| Manage User & Roles | Zarządzanie użytkownikami i rolami | | +| Invite users and add them to roles to manage access to your organization | Zaproś użytkowników i przypisz im role, aby zarządzać dostępem do Twojej organizacji | | +| Users | Użytkownicy | sidebar item | +| Roles | Role | sidebar item | +| **User tab** | | | +| Search users | Wyszukaj użytkownika | placeholder | +| Invite User | Zaproś użytkownika | addbutton | +| Email | Email | table header | +| Status | Status | table header | +| Role | Rola | table header | +| Confirmed | Zatwierdzony | account status | +| Not confirmed (?) | Niezatwierdzony (?) | unknown for me account status | +| Owner | Właściciel | role | +| Admin | Administrator | role | +| Member | Użytkownik | role | +| **Roles Tab** | | | +| Search roles | Wyszukaj role | placeholder | +| Add Role | Dodaj role | addbutton | +| Name | Nazwa | table header | +| Description | Opis | table header | +| Admin | Administrator | role | +| Member | Użytkownik | role | +| Admin role with the most permissions | Rola administratora z najszerszymi uprawnieniami | admin role desc | +| Members can only view resources | Członkowie mogą jedynie przeglądać zasoby | member role desc | + +##### Invite User popup + +| EN | PL | Notes | +| ----------------- | ------------------------------------------ | ----------- | +| Invite User | Give new users access to your organization | | +| Email | Email | | +| Enter an email | Wprowadź email | placeholder | +| Role | Rola | | +| Select role | Wybierz role | placeholder | +| Vaild for | Ważne do | | +| 1 day | Dzień | | +| 2 days | 2 dni | | +| 3 days | 3 dni | | +| 4 days | 4 dni | | +| 5 days | 5 dni | | +| 6 days | 6 dni | | +| 7 days | 7 dni | | +| Create Invitation | Utwórz zaproszenie | | +| Close | Zamknij | | + + +## Main “Shareable Links” +##### “Hero” section + +| EN | PL | Notes | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| Shareable Links | Łącza do udostępniania | | +| 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. | Twórz linki do udostępniania swoich zasobów. Linki zapewniają tymczasowy lub nieograniczony dostęp do zasobu. Możesz skonfigurować czas wygaśnięcia linku podczas jego tworzenia. | | +| Easy to create and share | Łatwe tworzenie i udostępnianie | | +| Configurable expiration duration | Konfigurowalny czas wygaśnięcia | | +| Secure and revocable | Bezpieczne i odwołalne | | +##### Content + +| EN | PL | Notes | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------- | +| Manage Shareable Links | Zarządzaj łączami do udostępniania | | +| Create shareable links to grant temporary or permament access to your resources | Utwórz łącze do udostępniania w celu przyznania tymczasowego lub stałego dostępu do zasobów | | +| Search links | Szukaj łączy | placeholder | +| Create Share Link | Utwórz nowe łącze | addbutton | +| Resource | Zasób | table header | +| Title | Tytuł | table header | +| Created | Utworzone | table header | +| Expires | Wygasa | table header | +| No links. Create one to get started. | Brak łączy. Utwórz, aby rozpocząć. | table placeholder | + +##### Create Shareable Link popup + +| EN | PL | Notes | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| Create Shareable Link | Utwórz łącze do udostępnienia | | +| Anyone with this link can access the resource | Każdy kto ma ten link może korzystać z zasobu | | +| Resource | Zasób | | +| Select resource | Wybierz zasób | | +| Search resources… | Szukaj zasobów… | resource selector popup | +| Title (optional) | Tytuł (opcjonalny) | | +| Enter title | Wprowadź tytuł | placeholder | +| Expire in | Wygasa za | | +| Minutes | Minut | | +| Hours | Godzin | | +| Days | Dni | | +| Months | Miesięcy | | +| Years | Lat | | +| Never expire | Nie wygasa | | +| 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. | Czas wygaśnięcia to okres, przez który link będzie aktywny i zapewni dostęp do zasobu. Po upływie tego czasu link przestanie działać, a użytkownicy, którzy go użyli, stracą dostęp do zasobu. | | +| Create Link | Utwórz łącze | | +| Close | Zamknij | | + + +## Main “General” + +| EN | PL | Notes | +| -------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------ | +| General | Ogólne | | +| Configure your organization’s general settings | Zarządzaj ogólnymi ustawieniami twoich organizacji | | +| General | Ogólne | sidebar item | +| Organization Settings | Ustawienia organizacji | | +| Manage your organization details and configuration | Zarządzaj szczegółami i konfiguracją organizacji | | +| Name | Nazwa | | +| This is the display name of the org | To jest wyświetlana nazwa Twojej organizacji | | +| Save Settings | Zapisz ustawienia | | +| Danger Zone | Niebezpieczna strefa | | +| Once you delete this org, there is no going back. Please be certain. | Jeśli usuniesz swoją tą organizację, nie ma odwrotu. Bądź ostrożny! | | +| Delete Organization Data | Usuń dane organizacji | | diff --git a/internationalization/tr.md b/internationalization/tr.md new file mode 100644 index 00000000..9e5bd274 --- /dev/null +++ b/internationalization/tr.md @@ -0,0 +1,310 @@ +## Authentication Site + +| EN | TR | Notes | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Pangolin Tarafından Destekleniyor | | +| Authentication Required | Kimlik Doğrulaması Gerekli | | +| Choose your preferred method to access {resource} | {resource}'a erişmek için tercih ettiğiniz yöntemi seçin | | +| PIN | PIN | | +| User | Kullanıcı | | +| 6-digit PIN Code | 6 haneli PIN Kodu | pin login | +| Login in with PIN | PIN ile Giriş Yap | pin login | +| Email | E-posta | user login | +| Enter your email | E-postanızı girin | user login | +| Password | Şifre | user login | +| Enter your password | Şifrenizi girin | user login | +| Forgot your password? | Şifrenizi mi unuttunuz? | user login | +| Log in | Giriş Yap | user login | + +--- + +## Login site + +| EN | TR | Notes | +| --------------------- | ------------------------------------------------------ | ----------- | +| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | | +| Log in to get started | Başlamak için giriş yapın | | +| Email | E-posta | | +| Enter your email | E-posta adresinizi girin | placeholder | +| Password | Şifre | | +| Enter your password | Şifrenizi girin | placeholder | +| Forgot your password? | Şifrenizi mi unuttunuz? | | +| Log in | Giriş Yap | | + +--- + +# Organization site after successful login + +| EN | TR | Notes | +| ----------------------------------------- | ------------------------------------------------------------------- | ----- | +| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | | +| You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | | + +--- + +## Shared Header, Navbar and Footer + +##### Header + +| EN | TR | Notes | +| ------------------- | -------------------------- | ----- | +| Documentation | Dokümantasyon | | +| Support | Destek | | +| Organization {name} | Organizasyon {name} | | + +##### Organization selector + +| EN | TR | Notes | +| ---------------- | ---------------------- | ----- | +| Search… | Ara… | | +| Create | Oluştur | | +| New Organization | Yeni Organizasyon | | +| Organizations | Organizasyonlar | | + +##### Navbar + +| EN | TR | Notes | +| --------------- | ------------------------------- | ----- | +| Sites | Siteler | | +| Resources | Kaynaklar | | +| User & Roles | Kullanıcılar ve Roller | | +| Shareable Links | Paylaşılabilir Linkler | | +| General | Genel | | + +##### Footer + +| EN | TR | Notes | +| ------------------------- | ------------------------------------------------ | -------------------- | +| Page {number} of {number} | Sayfa {number} / {number} | | +| Rows per page | Sayfa başına satırlar | | +| Pangolin | Pangolin | Footer'da yer alır | +| Built by Fossorial | Fossorial tarafından oluşturuldu | Footer'da yer alır | +| Open Source | Açık Kaynak | Footer'da yer alır | +| Documentation | Dokümantasyon | Footer'da yer alır | +| {version} | {version} | Footer'da yer alır | + +--- + +## Main “Sites” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----- | +| Newt (Recommended) | Newt (Tavsiye Edilen) | | +| 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. | En iyi kullanıcı deneyimi için Newt'i kullanın. Newt, arka planda WireGuard kullanır ve Pangolin kontrol paneli üzerinden özel ağınızdaki kaynaklarınıza LAN adresleriyle erişmenizi sağlar. | | +| Runs in Docker | Docker üzerinde çalışır | | +| Runs in shell on macOS, Linux, and Windows | macOS, Linux ve Windows’ta komut satırında çalışır | | +| Install Newt | Newt'i Yükle | | +| Basic WireGuard
| Temel WireGuard
| | +| Compatible with all WireGuard clients
| Tüm WireGuard istemcileriyle uyumlu
| | +| Manual configuration required | Manuel yapılandırma gereklidir | | + +##### Content + +| EN | TR | Notes | +| --------------------------------------------------------- | --------------------------------------------------------------------------- | ------------ | +| Manage Sites | Siteleri Yönet | | +| Allow connectivity to your network through secure tunnels | Güvenli tüneller aracılığıyla ağınıza bağlantı sağlayın | | +| Search sites | Siteleri ara | placeholder | +| Add Site | Site Ekle | | +| Name | Ad | Table Header | +| Online | Çevrimiçi | Table Header | +| Site | Site | Table Header | +| Data In | Gelen Veri | Table Header | +| Data Out | Giden Veri | Table Header | +| Connection Type | Bağlantı Türü | Table Header | +| Online | Çevrimiçi | Site state | +| Offline | Çevrimdışı | Site state | +| Edit → | Düzenle → | | +| View settings | Ayarları Görüntüle | Popup | +| Delete | Sil | Popup | + +##### Add Site Popup + +| EN | TR | Notes | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- | +| Create Site | Site Oluştur | | +| Create a new site to start connection for this site | Bu site için bağlantıyı başlatmak amacıyla yeni bir site oluşturun | | +| Name | Ad | | +| Site name | Site adı | placeholder | +| This is the name that will be displayed for this site. | Bu, site için görüntülenecek addır. | desc | +| Method | Yöntem | | +| Local | Yerel | | +| Newt | Newt | | +| WireGuard | WireGuard | | +| This is how you will expose connections. | Bağlantılarınızı bu şekilde açığa çıkaracaksınız. | | +| You will only be able to see the configuration once. | Yapılandırmayı yalnızca bir kez görüntüleyebilirsiniz. | | +| Learn how to install Newt on your system | Sisteminizde Newt'in nasıl kurulacağını öğrenin | | +| I have copied the config | Yapılandırmayı kopyaladım | | +| Create Site | Site Oluştur | | +| Close | Kapat | | + +--- + +## Main “Resources” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----- | +| Resources | Kaynaklar | | +| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Kaynaklar, özel ağınızda çalışan uygulamalar için proxy sunucularıdır. Özel ağınızdaki her HTTP veya HTTPS uygulaması için bir kaynak oluşturun. Her kaynağın, şifrelenmiş WireGuard tüneli üzerinden özel ve güvenli bağlantı sağlamak üzere bir siteyle ilişkili olması gerekir. | | +| Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | | +| Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | | +| User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | | + +##### Content + +| EN | TR | Notes | +| -------------------------------------------------- | ------------------------------------------------------------- | -------------------- | +| Manage Resources | Kaynakları Yönet | | +| Create secure proxies to your private applications | Özel uygulamalarınız için güvenli proxy’ler oluşturun | | +| Search resources | Kaynakları ara | placeholder | +| Name | Ad | | +| Site | Site | | +| Full URL | Tam URL | | +| Authentication | Kimlik Doğrulama | | +| Not Protected | Korunmayan | authentication state | +| Protected | Korunan | authentication state | +| Edit → | Düzenle → | | +| Add Resource | Kaynak Ekle | | + +##### Add Resource Popup + +| EN | TR | Notes | +| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------- | +| Create Resource | Kaynak Oluştur | | +| Create a new resource to proxy request to your app | Uygulamanıza gelen istekleri yönlendirmek için yeni bir kaynak oluşturun | | +| Name | Ad | | +| My Resource | Kaynağım | name placeholder | +| This is the name that will be displayed for this resource. | Bu, kaynağın görüntülenecek adıdır. | | +| Subdomain | Alt alan adı | | +| Enter subdomain | Alt alan adını girin | | +| This is the fully qualified domain name that will be used to access the resource. | Kaynağa erişmek için kullanılacak tam nitelikli alan adıdır. | | +| Site | Site | | +| Search site… | Site ara… | Site selector popup | +| This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | | +| Create Resource | Kaynak Oluştur | | +| Close | Kapat | | + +--- + +## Main “User & Roles” + +##### Content + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------- | +| Manage User & Roles | Kullanıcılar ve Rolleri Yönet | | +| Invite users and add them to roles to manage access to your organization | Organizasyonunuza erişimi yönetmek için kullanıcıları davet edin ve rollere atayın | | +| Users | Kullanıcılar | sidebar item | +| Roles | Roller | sidebar item | +| **User tab** | **Kullanıcı Sekmesi** | | +| Search users | Kullanıcıları ara | placeholder | +| Invite User | Kullanıcı Davet Et | addbutton | +| Email | E-posta | table header | +| Status | Durum | table header | +| Role | Rol | table header | +| Confirmed | Onaylandı | account status | +| Not confirmed (?) | Onaylanmadı (?) | account status | +| Owner | Sahip | role | +| Admin | Yönetici | role | +| Member | Üye | role | +| **Roles Tab** | **Roller Sekmesi** | | +| Search roles | Rolleri ara | placeholder | +| Add Role | Rol Ekle | addbutton | +| Name | Ad | table header | +| Description | Açıklama | table header | +| Admin | Yönetici | role | +| Member | Üye | role | +| Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc | +| Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc | + +##### Invite User popup + +| EN | TR | Notes | +| ----------------- | ----------------------------------------------------------------------- | ----------- | +| Invite User | Kullanıcı Davet Et | | +| Email | E-posta | | +| Enter an email | Bir e-posta adresi girin | placeholder | +| Role | Rol | | +| Select role | Rol seçin | placeholder | +| Gültig für | Geçerlilik Süresi | | +| 1 day | 1 gün | | +| 2 days | 2 gün | | +| 3 days | 3 gün | | +| 4 days | 4 gün | | +| 5 days | 5 gün | | +| 6 days | 6 gün | | +| 7 days | 7 gün | | +| Create Invitation | Davetiye Oluştur | | +| Close | Kapat | | + +--- + +## Main “Shareable Links” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----- | +| Shareable Links | Paylaşılabilir Bağlantılar | | +| 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. | Kaynaklarınıza paylaşılabilir bağlantılar oluşturun. Bağlantılar, kaynağınıza geçici veya sınırsız erişim sağlar. Oluştururken bağlantının geçerlilik süresini ayarlayabilirsiniz. | | +| Easy to create and share | Oluşturması ve paylaşması kolay | | +| Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | | +| Secure and revocable | Güvenli ve iptal edilebilir | | + +##### Content + +| EN | TR | Notes | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------- | +| Manage Shareable Links | Paylaşılabilir Bağlantıları Yönet | | +| Create shareable links to grant temporary or permanent access to your resources | Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun | | +| Search links | Bağlantıları ara | placeholder | +| Create Share Link | Bağlantı Oluştur | addbutton | +| Resource | Kaynak | table header | +| Title | Başlık | table header | +| Created | Oluşturulma Tarihi | table header | +| Expires | Son Kullanma Tarihi | table header | +| No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder | + +##### Create Shareable Link popup + +| EN | TR | Notes | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| Create Shareable Link | Paylaşılabilir Bağlantı Oluştur | | +| Anyone with this link can access the resource | Bu bağlantıya sahip olan herkes kaynağa erişebilir | | +| Resource | Kaynak | | +| Select resource | Kaynak seçin | | +| Search resources… | Kaynak ara… | resource selector popup | +| Title (optional) | Başlık (isteğe bağlı) | | +| Enter title | Başlık girin | placeholder | +| Expire in | Sona Erme Süresi | | +| Minutes | Dakika | | +| Hours | Saat | | +| Days | Gün | | +| Months | Ay | | +| Years | Yıl | | +| Never expire | Asla sona erme | | +| 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. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | | +| Create Link | Bağlantı Oluştur | | +| Close | Kapat | | + +--- + +## Main “General” + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ | +| General | Genel | | +| Configure your organization’s general settings | Organizasyonunuzun genel ayarlarını yapılandırın | | +| General | Genel | sidebar item | +| Organization Settings | Organizasyon Ayarları | | +| Manage your organization details and configuration | Organizasyonunuzun detaylarını ve yapılandırmasını yönetin | | +| Name | Ad | | +| This is the display name of the org | Bu, organizasyonunuzun görüntülenecek adıdır. | | +| Save Settings | Ayarları Kaydet | | +| Danger Zone | Tehlikeli Bölge | | +| Once you delete this org, there is no going back. Please be certain. | Bu organizasyonu sildikten sonra geri dönüş yoktur. Lütfen emin olun. | | +| Delete Organization Data | Organizasyon Verilerini Sil | | diff --git a/messages/bg-BG.json b/messages/bg-BG.json deleted file mode 100644 index 01af4db4..00000000 --- a/messages/bg-BG.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Създайте своя организация, сайт и ресурси", - "setupNewOrg": "Нова организация", - "setupCreateOrg": "Създаване на организация", - "setupCreateResources": "Създаване на ресурси", - "setupOrgName": "Име на организацията", - "orgDisplayName": "Това е публичното име на вашата организация.", - "orgId": "Идентификатор на организация", - "setupIdentifierMessage": "Това е уникалният идентификатор на вашата организация. Това е различно от публичното ѝ име.", - "setupErrorIdentifier": "Идентификаторът на организация вече е зает. Моля, изберете друг.", - "componentsErrorNoMemberCreate": "В момента не сте част от организация. Създайте организация, за да продължите.", - "componentsErrorNoMember": "В момента не сте част от организация.", - "welcome": "Добре дошли!", - "welcomeTo": "Добре дошли в", - "componentsCreateOrg": "Създаване на организация", - "componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.", - "componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.", - "dismiss": "Отхвърляне", - "componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.", - "componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!", - "inviteErrorNotValid": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е приета или вече не е валидна.", - "inviteErrorUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е предназначена за този потребител.", - "inviteLoginUser": "Моля, уверете се, че сте влезли като правилния потребител.", - "inviteErrorNoUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е за съществуващ потребител.", - "inviteCreateUser": "Моля, първо създайте акаунт.", - "goHome": "Отиди вкъщи", - "inviteLogInOtherUser": "Влезте като друг потребител", - "createAnAccount": "Създайте профил", - "inviteNotAccepted": "Поканата не е приета", - "authCreateAccount": "Създайте акаунт, за да започнете", - "authNoAccount": "Нямате акаунт?", - "email": "Имейл", - "password": "Парола", - "confirmPassword": "Потвърждение на паролата", - "createAccount": "Създаване на профил", - "viewSettings": "Преглед на настройките", - "delete": "Изтриване", - "name": "Име", - "online": "На линия", - "offline": "Извън линия", - "site": "Сайт", - "dataIn": "Входящ трафик", - "dataOut": "Изходящ трафик", - "connectionType": "Вид на връзката", - "tunnelType": "Вид на тунела", - "local": "Локална", - "edit": "Редактиране", - "siteConfirmDelete": "Потвърждение на изтриване на сайта", - "siteDelete": "Изтриване на сайта", - "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": "Локалните сайтове не тунелират, научете повече", - "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": "Сайтът е актуализиран.", - "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": "Архитектура", - "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": "Сигурни ли сте, че искате да изтриете трайно доставчика на идентичност {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": "Въведени тагове", - "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": "Hosted Pangolin", - "fossorial": "Fossorial", - "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": "Въведете пълния домейн на ресурса, за да видите наличните опции.", - "domainPickerDescriptionSaas": "Въведете пълен домейн, поддомейн или само име, за да видите наличните опции", - "domainPickerTabAll": "Всички", - "domainPickerTabOrganization": "Организация", - "domainPickerTabProvided": "Предоставен", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "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": "Съгласен съм с", - "termsOfService": "условията за ползване", - "and": "и", - "privacyPolicy": "политиката за поверителност" - }, - "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 Тайна парола", - "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": "Сайт Порт", - "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": "Управлявано Самостоятелно-хоствано", - "description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри", - "introTitle": "Управлявано Самостоятелно-хостван Панголиин", - "introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.", - "introDetail": "С тази опция все още управлявате свой собствен Панголиин възел — вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:", - "benefitSimplerOperations": { - "title": "По-прости операции", - "description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало." - }, - "benefitAutomaticUpdates": { - "title": "Автоматични актуализации", - "description": "Облачният панел за контрол се развива бързо, така че получавате нови функции и корекции на грешки без да се налага да извличате нови контейнери всеки път." - }, - "benefitLessMaintenance": { - "title": "По-малко поддръжка", - "description": "Няма миграции на база от данни, резервни копия или допълнителна инфраструктура за управление. Ние се грижим за това в облака." - }, - "benefitCloudFailover": { - "title": "Облачно преобръщане", - "description": "Ако вашият възел спре да работи, вашите тунели могат временно да преориентират към нашите облачни точки, докато не го възстановите." - }, - "benefitHighAvailability": { - "title": "Висока наличност (PoPs)", - "description": "Можете също така да прикрепите множество възли към вашия акаунт за резервно копиране и по-добра производителност." - }, - "benefitFutureEnhancements": { - "title": "Бъдещи подобрения", - "description": "Планираме да добавим още аналитични, алармиращи и управителни инструменти, за да направим вашето внедряване още по-здраво." - }, - "docsAlert": { - "text": "Научете повече за Управляваното Самостоятелно-хоствано опцията в нашата", - "documentation": "документация" - }, - "convertButton": "Конвертирайте този възел в Управлявано Самостоятелно-хоствано" - }, - "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 deleted file mode 100644 index 2eddea2a..00000000 --- a/messages/cs-CZ.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Vytvořte si organizaci, lokalitu a služby", - "setupNewOrg": "Nová organizace", - "setupCreateOrg": "Vytvořit organizaci", - "setupCreateResources": "Vytvořit zdroje", - "setupOrgName": "Název organizace", - "orgDisplayName": "Toto je zobrazovaný název vaší organizace.", - "orgId": "ID organizace", - "setupIdentifierMessage": "Toto je jedinečný identifikátor vaší organizace. Nemusí odpovídat názvu organizace.", - "setupErrorIdentifier": "ID organizace je již použito. Zvolte prosím jiné.", - "componentsErrorNoMemberCreate": "Zatím nejste členem žádné organizace. Abyste mohli začít, vytvořte si organizaci.", - "componentsErrorNoMember": "Zatím nejste členem žádných organizací.", - "welcome": "Vítejte!", - "welcomeTo": "Vítejte v", - "componentsCreateOrg": "Vytvořte organizaci", - "componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.", - "componentsInvalidKey": "Byly nalezeny neplatné nebo propadlé licenční klíče. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.", - "dismiss": "Zavřít", - "componentsLicenseViolation": "Porušení licenčních podmínek: Tento server používá {usedSites} stránek, což překračuje limit {maxSites} licencovaných stránek. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.", - "componentsSupporterMessage": "Děkujeme, že podporujete Pangolin jako {tier}!", - "inviteErrorNotValid": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, nebyla přijata nebo již není platná.", - "inviteErrorUser": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, není pro tohoto uživatele.", - "inviteLoginUser": "Prosím ujistěte se, že jste přihlášeni jako správný uživatel.", - "inviteErrorNoUser": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, není pro existujícího uživatele.", - "inviteCreateUser": "Nejprve si prosím vytvořte účet.", - "goHome": "Přejít na hlavní stránku", - "inviteLogInOtherUser": "Přihlásit se jako jiný uživatel", - "createAnAccount": "Vytvořit účet", - "inviteNotAccepted": "Pozvánka nebyla přijata", - "authCreateAccount": "Vytvořte si účet, abyste mohli začít", - "authNoAccount": "Nemáte účet?", - "email": "Email", - "password": "Heslo", - "confirmPassword": "Potvrďte heslo", - "createAccount": "Vytvořit účet", - "viewSettings": "Zobrazit nastavení", - "delete": "Odstranit", - "name": "Jméno", - "online": "Online", - "offline": "Offline", - "site": "Lokalita", - "dataIn": "Přijatá data", - "dataOut": "Odeslaná data", - "connectionType": "Typ připojení", - "tunnelType": "Typ tunelu", - "local": "Místní", - "edit": "Upravit", - "siteConfirmDelete": "Potvrdit odstranění lokality", - "siteDelete": "Odstranění lokality", - "siteMessageRemove": "Jakmile lokalitu odstraníte, nebude dostupná. Všechny související služby a cíle budou také odstraněny.", - "siteMessageConfirm": "Pro potvrzení zadejte prosím název lokality.", - "siteQuestionRemove": "Opravdu chcete odstranit lokalitu {selectedSite} z organizace?", - "siteManageSites": "Správa lokalit", - "siteDescription": "Umožní připojení k vaší síti prostřednictvím zabezpečených tunelů", - "siteCreate": "Vytvořit lokalitu", - "siteCreateDescription2": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili novou lokalitu", - "siteCreateDescription": "Vytvořte novou lokalitu, abyste mohli začít připojovat služby", - "close": "Zavřít", - "siteErrorCreate": "Chyba při vytváření lokality", - "siteErrorCreateKeyPair": "Nebyly nalezeny klíče nebo výchozí nastavení lokality", - "siteErrorCreateDefaults": "Výchozí nastavení lokality nenalezeno", - "method": "Způsob", - "siteMethodDescription": "Tímto způsobem budete vystavovat spojení.", - "siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém", - "siteSeeConfigOnce": "Konfiguraci uvidíte pouze jednou.", - "siteLoadWGConfig": "Načítání konfigurace WireGuard...", - "siteDocker": "Rozbalit pro detaily nasazení v Dockeru", - "toggle": "Přepínač", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "Místní lokality se netunelují, dozvědět se více", - "siteConfirmCopy": "Konfiguraci jsem zkopíroval", - "searchSitesProgress": "Hledat lokality...", - "siteAdd": "Přidat lokalitu", - "siteInstallNewt": "Nainstalovat Newt", - "siteInstallNewtDescription": "Spustit Newt na vašem systému", - "WgConfiguration": "Konfigurace WireGuard", - "WgConfigurationDescription": "Použijte následující konfiguraci pro připojení k vaší síti", - "operatingSystem": "Operační systém", - "commands": "Příkazy", - "recommended": "Doporučeno", - "siteNewtDescription": "Ideálně použijte Newt, který využívá WireGuard a umožňuje adresovat vaše soukromé zdroje pomocí jejich LAN adresy ve vaší privátní síti přímo z dashboardu Pangolin.", - "siteRunsInDocker": "Běží v Dockeru", - "siteRunsInShell": "Běží v shellu na macOS, Linuxu a Windows", - "siteErrorDelete": "Chyba při odstraňování lokality", - "siteErrorUpdate": "Nepodařilo se upravit lokalitu", - "siteErrorUpdateDescription": "Při úpravě lokality došlo k chybě.", - "siteUpdated": "Lokalita upravena", - "siteUpdatedDescription": "Lokalita byla upravena.", - "siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu", - "siteSettingDescription": "Upravte nastavení vaší lokality", - "siteSetting": "Nastavení {siteName}", - "siteNewtTunnel": "Tunel Newt (doporučeno)", - "siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do vaší sítě. Žádné další nastavení.", - "siteWg": "Základní WireGuard", - "siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.", - "siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", - "siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.", - "siteLocalDescriptionSaas": "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", - "siteNewtCredentialsDescription": "Tímto způsobem se bude Newt autentizovat na serveru", - "siteCredentialsSave": "Uložit přihlašovací údaje", - "siteCredentialsSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.", - "siteInfo": "Údaje o lokalitě", - "status": "Stav", - "shareTitle": "Spravovat sdílení odkazů", - "shareDescription": "Vytvořte odkazy, abyste udělili dočasný nebo trvalý přístup k vašim zdrojům", - "shareSearch": "Hledat sdílené odkazy...", - "shareCreate": "Vytvořit odkaz", - "shareErrorDelete": "Nepodařilo se odstranit odkaz", - "shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu", - "shareDeleted": "Odkaz odstraněn", - "shareDeletedDescription": "Odkaz byl odstraněn", - "shareTokenDescription": "Váš přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientem v každé žádosti o ověřený přístup.", - "accessToken": "Přístupový token", - "usageExamples": "Příklady použití", - "tokenId": "ID tokenu", - "requestHeades": "Hlavičky požadavku", - "queryParameter": "Parametry dotazu", - "importantNote": "Důležité upozornění", - "shareImportantDescription": "Z bezpečnostních důvodů je doporučeno používat raději hlavičky než parametry dotazu pokud je to možné, protože parametry dotazu mohou být zaznamenány v logu serveru nebo v historii prohlížeče.", - "token": "Token", - "shareTokenSecurety": "Uchovejte přístupový token v bezpečí. Nesdílejte jej na veřejně přístupných místěch nebo v kódu na straně klienta.", - "shareErrorFetchResource": "Nepodařilo se načíst zdroje", - "shareErrorFetchResourceDescription": "Při načítání zdrojů došlo k chybě", - "shareErrorCreate": "Nepodařilo se vytvořit odkaz", - "shareErrorCreateDescription": "Při vytváření odkazu došlo k chybě", - "shareCreateDescription": "Kdokoliv s tímto odkazem může přistupovat ke zdroji", - "shareTitleOptional": "Název (volitelné)", - "expireIn": "Platnost vyprší za", - "neverExpire": "Nikdy nevyprší", - "shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.", - "shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Ujistěte se, že jste jej zkopírovali.", - "shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.", - "shareTokenUsage": "Zobrazit využití přístupového tokenu", - "createLink": "Vytvořit odkaz", - "resourcesNotFound": "Nebyly nalezeny žádné zdroje", - "resourceSearch": "Vyhledat zdroje", - "openMenu": "Otevřít nabídku", - "resource": "Zdroj", - "title": "Název", - "created": "Vytvořeno", - "expires": "Vyprší", - "never": "Nikdy", - "shareErrorSelectResource": "Zvolte prosím zdroj", - "resourceTitle": "Spravovat zdroje", - "resourceDescription": "Vytvořte bezpečné proxy služby pro přístup k privátním aplikacím", - "resourcesSearch": "Prohledat zdroje...", - "resourceAdd": "Přidat zdroj", - "resourceErrorDelte": "Chyba při odstraňování zdroje", - "authentication": "Autentifikace", - "protected": "Chráněno", - "notProtected": "Nechráněno", - "resourceMessageRemove": "Jakmile zdroj odstraníte, nebude dostupný. Všechny související služby a cíle budou také odstraněny.", - "resourceMessageConfirm": "Pro potvrzení zadejte prosím název zdroje.", - "resourceQuestionRemove": "Opravdu chcete odstranit zdroj {selectedResource} z organizace?", - "resourceHTTP": "Zdroj HTTPS", - "resourceHTTPDescription": "Požadavky na proxy pro vaši aplikaci přes HTTPS pomocí subdomény nebo základní domény.", - "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", - "resourceInfo": "Informace o zdroji", - "resourceNameDescription": "Toto je zobrazovaný název zdroje.", - "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", - "resourceHTTPSSettings": "Nastavení HTTPS", - "resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude váš zdroj přístupný přes HTTPS", - "domainType": "Typ domény", - "subdomain": "Subdoména", - "baseDomain": "Základní doména", - "subdomnainDescription": "Subdoména, kde bude váš zdroj přístupný.", - "resourceRawSettings": "Nastavení TCP/UDP", - "resourceRawSettingsDescription": "Nakonfigurujte, jak bude váš dokument přístupný přes TCP/UDP", - "protocol": "Protokol", - "protocolSelect": "Vybrat protokol", - "resourcePortNumber": "Číslo portu", - "resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.", - "cancel": "Zrušit", - "resourceConfig": "Konfigurační snippety", - "resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační snippety pro nastavení TCP/UDP zdroje", - "resourceAddEntrypoints": "Traefik: Přidat vstupní body", - "resourceExposePorts": "Gerbil: Expose Ports in Docker Compose", - "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": "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": "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": "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": "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": "Cíl s těmito nastaveními již existuje", - "targetWireGuardErrorInvalidIp": "Invalid target IP", - "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "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": "Odstranit cíl", - "actionGetTarget": "Získat cíl", - "actionListTargets": "Seznam cílů", - "actionUpdateTarget": "Update Target", - "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": "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": "Popisky", - "containerLabelsCount": "{count, plural, one {# štítek} other {# štítků}}", - "containerLabelsTitle": "Popisky kontejneru", - "containerLabelEmpty": "", - "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 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": "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": "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": "Souhlasím s", - "termsOfService": "podmínky služby", - "and": "a", - "privacyPolicy": "zásady ochrany osobních údajů" - }, - "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 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": "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": "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": "Port webu", - "createInternalResourceDialogSitePortDescription": "Použijte tento port pro přístup ke zdroji na webu při připojení s klientem.", - "createInternalResourceDialogTargetConfiguration": "Target Configuration", - "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": "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": "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": "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": "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": "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": "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": "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": "Další informace o možnostech Managed Self-Hosted v našem", - "documentation": "dokumentace" - }, - "convertButton": "Převést tento uzel na spravovaný vlastní hostitel" - }, - "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", - "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 deleted file mode 100644 index fa534c77..00000000 --- a/messages/de-DE.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen", - "setupNewOrg": "Neue Organisation", - "setupCreateOrg": "Organisation erstellen", - "setupCreateResources": "Ressource erstellen", - "setupOrgName": "Name der Organisation", - "orgDisplayName": "Anzeigename der Organisation.", - "orgId": "Organisations-ID", - "setupIdentifierMessage": "Dies ist eine Eindeutige ID für Ihre Organisation. Diese ist unabhängig vom Anzeigenamen.", - "setupErrorIdentifier": "Organisations-ID ist bereits vergeben. Bitte wähle eine andere.", - "componentsErrorNoMemberCreate": "Du bist derzeit kein Mitglied einer Organisation. Erstelle eine Organisation, um zu starten.", - "componentsErrorNoMember": "Du bist aktuell kein Mitglied einer Organisation.", - "welcome": "Willkommen zu Pangolin", - "welcomeTo": "Willkommen bei", - "componentsCreateOrg": "Erstelle eine Organisation", - "componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.", - "componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", - "dismiss": "Verwerfen", - "componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.", - "componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!", - "inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.", - "inviteErrorUser": "Es tut uns leid, aber es scheint, als sei die Einladung, auf die du zugreifen möchtest, nicht für diesen Benutzer bestimmt.", - "inviteLoginUser": "Bitte stelle sicher, dass du als korrekter Benutzer angemeldet bist.", - "inviteErrorNoUser": "Es tut uns leid, aber es sieht so aus, als sei die Einladung, auf die du zugreifen möchtest, nicht für einen existierenden Benutzer bestimmt.", - "inviteCreateUser": "Bitte erstelle zuerst ein Konto.", - "goHome": "Zur Startseite", - "inviteLogInOtherUser": "Als anderer Benutzer anmelden", - "createAnAccount": "Konto erstellen", - "inviteNotAccepted": "Einladung nicht angenommen", - "authCreateAccount": "Erstellen ein Konto um loszulegen", - "authNoAccount": "Du besitzt noch kein Konto?", - "email": "E-Mail", - "password": "Passwort", - "confirmPassword": "Passwort bestätigen", - "createAccount": "Konto erstellen", - "viewSettings": "Einstellungen anzeigen", - "delete": "Löschen", - "name": "Name", - "online": "Online", - "offline": "Offline", - "site": "Standort", - "dataIn": "Daten eingehend", - "dataOut": "Daten ausgehend", - "connectionType": "Verbindungstyp", - "tunnelType": "Tunneltyp", - "local": "Lokal", - "edit": "Bearbeiten", - "siteConfirmDelete": "Standort löschen bestätigen", - "siteDelete": "Standort löschen", - "siteMessageRemove": "Sobald dieser Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit diesem Standort verbunden sind, werden ebenfalls entfernt.", - "siteMessageConfirm": "Um zu bestätigen, gib den Namen des Standortes unten ein.", - "siteQuestionRemove": "Bist du sicher, dass der Standort {selectedSite} aus der Organisation entfernt werden soll?", - "siteManageSites": "Standorte verwalten", - "siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben", - "siteCreate": "Standort erstellen", - "siteCreateDescription2": "Folge den nachfolgenden Schritten, um einen neuen Standort zu erstellen und zu verbinden", - "siteCreateDescription": "Erstelle einen neuen Standort, um Ressourcen zu verbinden", - "close": "Schließen", - "siteErrorCreate": "Fehler beim Erstellen des Standortes", - "siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden", - "siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden", - "method": "Methode", - "siteMethodDescription": "So werden Verbindungen freigegeben.", - "siteLearnNewt": "Wie du Newt auf deinem System installieren kannst", - "siteSeeConfigOnce": "Du kannst die Konfiguration nur einmalig ansehen.", - "siteLoadWGConfig": "Lade WireGuard Konfiguration...", - "siteDocker": "Erweitern für Docker Details", - "toggle": "Umschalten", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "Mehr Infos zu lokalen Sites", - "siteConfirmCopy": "Ich habe die Konfiguration kopiert", - "searchSitesProgress": "Standorte durchsuchen...", - "siteAdd": "Standort hinzufügen", - "siteInstallNewt": "Newt installieren", - "siteInstallNewtDescription": "Installiere Newt auf deinem System.", - "WgConfiguration": "WireGuard Konfiguration", - "WgConfigurationDescription": "Verwende folgende Konfiguration, um dich mit deinem Netzwerk zu verbinden", - "operatingSystem": "Betriebssystem", - "commands": "Befehle", - "recommended": "Empfohlen", - "siteNewtDescription": "Nutze Newt für die beste Benutzererfahrung. Newt verwendet WireGuard as Basis und erlaubt Ihnen, Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk aus dem Pangolin-Dashboard heraus zu adressieren.", - "siteRunsInDocker": "Läuft in Docker", - "siteRunsInShell": "Läuft in der Konsole auf macOS, Linux und Windows", - "siteErrorDelete": "Fehler beim Löschen des Standortes", - "siteErrorUpdate": "Fehler beim Aktualisieren des Standortes", - "siteErrorUpdateDescription": "Beim Aktualisieren des Standortes ist ein Fehler aufgetreten.", - "siteUpdated": "Standort aktualisiert", - "siteUpdatedDescription": "Der Standort wurde aktualisiert.", - "siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren", - "siteSettingDescription": "Konfigurieren der Standort Einstellungen", - "siteSetting": "{siteName} Einstellungen", - "siteNewtTunnel": "Newt-Tunnel (empfohlen)", - "siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.", - "siteWg": "Einfacher WireGuard Tunnel", - "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", - "siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", - "siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", - "siteSeeAll": "Alle Standorte anzeigen", - "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", - "siteNewtCredentials": "Neue Newt Zugangsdaten", - "siteNewtCredentialsDescription": "So wird sich Newt mit dem Server authentifizieren", - "siteCredentialsSave": "Ihre Zugangsdaten speichern", - "siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.", - "siteInfo": "Standort-Informationen", - "status": "Status", - "shareTitle": "Links zum Teilen verwalten", - "shareDescription": "Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren", - "shareSearch": "Freigabe-Links suchen...", - "shareCreate": "Link erstellen", - "shareErrorDelete": "Link konnte nicht gelöscht werden", - "shareErrorDeleteMessage": "Fehler beim Löschen des Links", - "shareDeleted": "Link gelöscht", - "shareDeletedDescription": "Der Link wurde gelöscht", - "shareTokenDescription": "Ihr Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Anfrage-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.", - "accessToken": "Zugangs-Token", - "usageExamples": "Nutzungsbeispiele", - "tokenId": "Token-ID", - "requestHeades": "Anfrage-Header", - "queryParameter": "Abfrageparameter", - "importantNote": "Wichtige Notiz", - "shareImportantDescription": "Aus Sicherheitsgründen wird die Verwendung von Headern über Abfrageparameter empfohlen, wenn möglich, da Abfrageparameter in Server-Logs oder Browserverlauf protokolliert werden können.", - "token": "Token", - "shareTokenSecurety": "Halten Sie Ihr Zugangs-Token sicher. Teilen Sie es nicht in öffentlich zugänglichen Bereichen oder Client-seitigem Code.", - "shareErrorFetchResource": "Fehler beim Abrufen der Ressourcen", - "shareErrorFetchResourceDescription": "Beim Abrufen der Ressourcen ist ein Fehler aufgetreten", - "shareErrorCreate": "Fehler beim Erstellen des Teilen-Links", - "shareErrorCreateDescription": "Beim Erstellen des Teilen-Links ist ein Fehler aufgetreten", - "shareCreateDescription": "Jeder mit diesem Link kann auf die Ressource zugreifen", - "shareTitleOptional": "Titel (optional)", - "expireIn": "Verfällt in", - "neverExpire": "Nie ablaufen", - "shareExpireDescription": "Ablaufzeit ist, wie lange der Link verwendet werden kann und bietet Zugriff auf die Ressource. Nach dieser Zeit wird der Link nicht mehr funktionieren und Benutzer, die diesen Link benutzt haben, verlieren den Zugriff auf die Ressource.", - "shareSeeOnce": "Sie können diese Linie nur sehen. Bitte kopieren Sie sie.", - "shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.", - "shareTokenUsage": "Zugriffstoken-Nutzung anzeigen", - "createLink": "Link erstellen", - "resourcesNotFound": "Keine Ressourcen gefunden", - "resourceSearch": "Suche Ressourcen", - "openMenu": "Menü öffnen", - "resource": "Ressource", - "title": "Titel", - "created": "Erstellt", - "expires": "Gültig bis", - "never": "Nie", - "shareErrorSelectResource": "Bitte wählen Sie eine Ressource", - "resourceTitle": "Ressourcen verwalten", - "resourceDescription": "Erstellen Sie sichere Proxies für Ihre privaten Anwendungen", - "resourcesSearch": "Suche Ressourcen...", - "resourceAdd": "Ressource hinzufügen", - "resourceErrorDelte": "Fehler beim Löschen der Ressource", - "authentication": "Authentifizierung", - "protected": "Geschützt", - "notProtected": "Nicht geschützt", - "resourceMessageRemove": "Einmal entfernt, wird die Ressource nicht mehr zugänglich sein. Alle mit der Ressource verbundenen Ziele werden ebenfalls entfernt.", - "resourceMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen der Ressource unten ein.", - "resourceQuestionRemove": "Sind Sie sicher, dass Sie die Ressource {selectedResource} aus der Organisation entfernen möchten?", - "resourceHTTP": "HTTPS-Ressource", - "resourceHTTPDescription": "Proxy-Anfragen an Ihre App über HTTPS unter Verwendung einer Subdomain oder einer Basis-Domain.", - "resourceRaw": "Rohe TCP/UDP Ressource", - "resourceRawDescription": "Proxy-Anfragen an Ihre App über TCP/UDP mit einer Portnummer.", - "resourceCreate": "Ressource erstellen", - "resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen", - "resourceSeeAll": "Alle Ressourcen anzeigen", - "resourceInfo": "Ressourcen-Informationen", - "resourceNameDescription": "Dies ist der Anzeigename für die Ressource.", - "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", - "resourceHTTPSSettings": "HTTPS-Einstellungen", - "resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf Ihre Ressource über HTTPS", - "domainType": "Domänentyp", - "subdomain": "Subdomain", - "baseDomain": "Basisdomäne", - "subdomnainDescription": "Die Subdomäne, auf die Ihre Ressource zugegriffen werden soll.", - "resourceRawSettings": "TCP/UDP Einstellungen", - "resourceRawSettingsDescription": "Konfigurieren Sie den Zugriff auf Ihre Ressource über TCP/UDP", - "protocol": "Protokoll", - "protocolSelect": "Wählen Sie ein Protokoll", - "resourcePortNumber": "Portnummer", - "resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.", - "cancel": "Abbrechen", - "resourceConfig": "Konfiguration Snippets", - "resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um Ihre TCP/UDP Ressource einzurichten", - "resourceAddEntrypoints": "Traefik: Einstiegspunkte hinzufügen", - "resourceExposePorts": "Gerbil: Ports im Docker Compose ausblenden", - "resourceLearnRaw": "Lernen Sie, wie Sie TCP/UDP Ressourcen konfigurieren", - "resourceBack": "Zurück zu den Ressourcen", - "resourceGoTo": "Zu Ressource gehen", - "resourceDelete": "Ressource löschen", - "resourceDeleteConfirm": "Ressource löschen bestätigen", - "visibility": "Sichtbarkeit", - "enabled": "Aktiviert", - "disabled": "Deaktiviert", - "general": "Allgemein", - "generalSettings": "Allgemeine Einstellungen", - "proxy": "Proxy", - "internal": "Intern", - "rules": "Regeln", - "resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource", - "resourceSetting": "{resourceName} Einstellungen", - "alwaysAllow": "Immer erlauben", - "alwaysDeny": "Immer ablehnen", - "passToAuth": "Weiterleiten zur Authentifizierung", - "orgSettingsDescription": "Konfiguriere die allgemeinen Einstellungen deiner Organisation", - "orgGeneralSettings": "Organisations-Einstellungen", - "orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten", - "saveGeneralSettings": "Allgemeine Einstellungen speichern", - "saveSettings": "Einstellungen speichern", - "orgDangerZone": "Gefahrenzone", - "orgDangerZoneDescription": "Sobald Sie diesen Org löschen, gibt es kein Zurück mehr. Bitte seien Sie vorsichtig.", - "orgDelete": "Organisation löschen", - "orgDeleteConfirm": "Organisation löschen bestätigen", - "orgMessageRemove": "Diese Aktion ist unwiderruflich und löscht alle zugehörigen Daten.", - "orgMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen der Organisation unten ein.", - "orgQuestionRemove": "Sind Sie sicher, dass Sie die Organisation {selectedOrg} entfernen möchten?", - "orgUpdated": "Organisation aktualisiert", - "orgUpdatedDescription": "Die Organisation wurde aktualisiert.", - "orgErrorUpdate": "Fehler beim Aktualisieren der Organisation", - "orgErrorUpdateMessage": "Beim Aktualisieren der Organisation ist ein Fehler aufgetreten.", - "orgErrorFetch": "Fehler beim Abrufen von Organisationen", - "orgErrorFetchMessage": "Beim Auflisten Ihrer Organisationen ist ein Fehler aufgetreten", - "orgErrorDelete": "Organisation konnte nicht gelöscht werden", - "orgErrorDeleteMessage": "Beim Löschen der Organisation ist ein Fehler aufgetreten.", - "orgDeleted": "Organisation gelöscht", - "orgDeletedMessage": "Die Organisation und ihre Daten wurden gelöscht.", - "orgMissing": "Organisations-ID fehlt", - "orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.", - "accessUsersManage": "Benutzer verwalten", - "accessUsersDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf deine Organisation zu verwalten", - "accessUsersSearch": "Benutzer suchen...", - "accessUserCreate": "Benutzer erstellen", - "accessUserRemove": "Benutzer entfernen", - "username": "Benutzername", - "identityProvider": "Identitätsanbieter", - "role": "Rolle", - "nameRequired": "Name ist erforderlich", - "accessRolesManage": "Rollen verwalten", - "accessRolesDescription": "Konfigurieren Sie Rollen, um den Zugriff auf Ihre Organisation zu verwalten", - "accessRolesSearch": "Rollen suchen...", - "accessRolesAdd": "Rolle hinzufügen", - "accessRoleDelete": "Rolle löschen", - "description": "Beschreibung", - "inviteTitle": "Einladungen öffnen", - "inviteDescription": "Ihre Einladungen an andere Benutzer verwalten", - "inviteSearch": "Einladungen suchen...", - "minutes": "Minuten", - "hours": "Stunden", - "days": "Tage", - "weeks": "Wochen", - "months": "Monate", - "years": "Jahre", - "day": "{count, plural, one {# Tag} other {# Tage}}", - "apiKeysTitle": "API-Schlüssel Information", - "apiKeysConfirmCopy2": "Sie müssen bestätigen, dass Sie den API-Schlüssel kopiert haben.", - "apiKeysErrorCreate": "Fehler beim Erstellen des API-Schlüssels", - "apiKeysErrorSetPermission": "Fehler beim Setzen der Berechtigungen", - "apiKeysCreate": "API-Schlüssel generieren", - "apiKeysCreateDescription": "Generieren Sie einen neuen API-Schlüssel für Ihre Organisation", - "apiKeysGeneralSettings": "Berechtigungen", - "apiKeysGeneralSettingsDescription": "Legen Sie fest, was dieser API-Schlüssel tun kann", - "apiKeysList": "Ihr API-Schlüssel", - "apiKeysSave": "Speichern Sie Ihren API-Schlüssel", - "apiKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", - "apiKeysInfo": "Ihr API-Schlüssel ist:", - "apiKeysConfirmCopy": "Ich habe den API-Schlüssel kopiert", - "generate": "Generieren", - "done": "Fertig", - "apiKeysSeeAll": "Alle API-Schlüssel anzeigen", - "apiKeysPermissionsErrorLoadingActions": "Fehler beim Laden der API-Schlüsselaktionen", - "apiKeysPermissionsErrorUpdate": "Fehler beim Setzen der Berechtigungen", - "apiKeysPermissionsUpdated": "Berechtigungen aktualisiert", - "apiKeysPermissionsUpdatedDescription": "Die Berechtigungen wurden aktualisiert.", - "apiKeysPermissionsGeneralSettings": "Berechtigungen", - "apiKeysPermissionsGeneralSettingsDescription": "Legen Sie fest, was dieser API-Schlüssel tun kann", - "apiKeysPermissionsSave": "Berechtigungen speichern", - "apiKeysPermissionsTitle": "Berechtigungen", - "apiKeys": "API-Schlüssel", - "searchApiKeys": "API-Schlüssel suchen...", - "apiKeysAdd": "API-Schlüssel generieren", - "apiKeysErrorDelete": "Fehler beim Löschen des API-Schlüssels", - "apiKeysErrorDeleteMessage": "Fehler beim Löschen des API-Schlüssels", - "apiKeysQuestionRemove": "Sind Sie sicher, dass Sie den API-Schlüssel {selectedApiKey} aus der Organisation entfernen möchten?", - "apiKeysMessageRemove": "Einmal entfernt, kann der API-Schlüssel nicht mehr verwendet werden.", - "apiKeysMessageConfirm": "Zur Bestätigung geben Sie bitte den Namen des API-Schlüssels unten ein.", - "apiKeysDeleteConfirm": "Löschen des API-Schlüssels bestätigen", - "apiKeysDelete": "API-Schlüssel löschen", - "apiKeysManage": "API-Schlüssel verwalten", - "apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet", - "apiKeysSettings": "{apiKeyName} Einstellungen", - "userTitle": "Alle Benutzer verwalten", - "userDescription": "Alle Benutzer im System anzeigen und verwalten", - "userAbount": "Über Benutzerverwaltung", - "userAbountDescription": "Diese Tabelle zeigt alle root-Benutzerobjekte im System an. Jeder Benutzer kann zu mehreren Organisationen gehören. Das Entfernen eines Benutzers aus einer Organisation löscht nicht sein Root-Benutzerobjekt - er bleibt im System. Um einen Benutzer komplett aus dem System zu entfernen, müssen Sie sein Root-Benutzerobjekt mit der Lösch-Aktion in dieser Tabelle löschen.", - "userServer": "Server Benutzer", - "userSearch": "Serverbenutzer suchen...", - "userErrorDelete": "Fehler beim Löschen des Benutzers", - "userDeleteConfirm": "Benutzer löschen bestätigen", - "userDeleteServer": "Benutzer vom Server löschen", - "userMessageRemove": "Der Benutzer wird von allen Organisationen entfernt und vollständig vom Server entfernt.", - "userMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Benutzers unten ein.", - "userQuestionRemove": "Sind Sie sicher, dass Sie {selectedUser} dauerhaft vom Server löschen möchten?", - "licenseKey": "Lizenzschlüssel", - "valid": "Gültig", - "numberOfSites": "Anzahl der Standorte", - "licenseKeySearch": "Lizenzschlüssel suchen...", - "licenseKeyAdd": "Lizenzschlüssel hinzufügen", - "type": "Typ", - "licenseKeyRequired": "Lizenzschlüssel ist erforderlich", - "licenseTermsAgree": "Sie müssen den Lizenzbedingungen zustimmen", - "licenseErrorKeyLoad": "Fehler beim Laden der Lizenzschlüssel", - "licenseErrorKeyLoadDescription": "Beim Laden der Lizenzschlüssel ist ein Fehler aufgetreten.", - "licenseErrorKeyDelete": "Fehler beim Löschen des Lizenzschlüssels", - "licenseErrorKeyDeleteDescription": "Beim Löschen des Lizenzschlüssels ist ein Fehler aufgetreten.", - "licenseKeyDeleted": "Lizenzschlüssel gelöscht", - "licenseKeyDeletedDescription": "Der Lizenzschlüssel wurde gelöscht.", - "licenseErrorKeyActivate": "Fehler beim Aktivieren des Lizenzschlüssels", - "licenseErrorKeyActivateDescription": "Beim Aktivieren des Lizenzschlüssels ist ein Fehler aufgetreten.", - "licenseAbout": "Über Lizenzierung", - "communityEdition": "Community-Edition", - "licenseAboutDescription": "Dies ist für Geschäfts- und Unternehmensanwender, die Pangolin in einem kommerziellen Umfeld einsetzen. Wenn Sie Pangolin für den persönlichen Gebrauch verwenden, können Sie diesen Abschnitt ignorieren.", - "licenseKeyActivated": "Lizenzschlüssel aktiviert", - "licenseKeyActivatedDescription": "Der Lizenzschlüssel wurde erfolgreich aktiviert.", - "licenseErrorKeyRecheck": "Fehler beim Überprüfen der Lizenzschlüssel", - "licenseErrorKeyRecheckDescription": "Ein Fehler trat auf beim Wiederherstellen der Lizenzschlüssel.", - "licenseErrorKeyRechecked": "Lizenzschlüssel neu geladen", - "licenseErrorKeyRecheckedDescription": "Alle Lizenzschlüssel wurden neu geladen", - "licenseActivateKey": "Lizenzschlüssel aktivieren", - "licenseActivateKeyDescription": "Geben Sie einen Lizenzschlüssel ein, um ihn zu aktivieren.", - "licenseActivate": "Lizenz aktivieren", - "licenseAgreement": "Durch Ankreuzung dieses Kästchens bestätigen Sie, dass Sie die Lizenzbedingungen gelesen und akzeptiert haben, die mit dem Lizenzschlüssel in Verbindung stehen.", - "fossorialLicense": "Fossorial Gewerbelizenz & Abonnementbedingungen anzeigen", - "licenseMessageRemove": "Dadurch werden der Lizenzschlüssel und alle zugehörigen Berechtigungen entfernt.", - "licenseMessageConfirm": "Um zu bestätigen, geben Sie bitte den Lizenzschlüssel unten ein.", - "licenseQuestionRemove": "Sind Sie sicher, dass Sie den Lizenzschlüssel {selectedKey} löschen möchten?", - "licenseKeyDelete": "Lizenzschlüssel löschen", - "licenseKeyDeleteConfirm": "Lizenzschlüssel löschen bestätigen", - "licenseTitle": "Lizenzstatus verwalten", - "licenseTitleDescription": "Lizenzschlüssel im System anzeigen und verwalten", - "licenseHost": "Hostlizenz", - "licenseHostDescription": "Verwalten Sie den Haupt-Lizenzschlüssel für den Host.", - "licensedNot": "Nicht lizenziert", - "hostId": "Host-ID", - "licenseReckeckAll": "Überprüfe alle Schlüssel", - "licenseSiteUsage": "Standort-Nutzung", - "licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Standorte an, die diese Lizenz verwenden.", - "licenseNoSiteLimit": "Die Anzahl der Standorte, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.", - "licensePurchase": "Lizenz kaufen", - "licensePurchaseSites": "Zusätzliche Standorte kaufen\n", - "licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet", - "licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.", - "licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}", - "licenseFee": "Lizenzgebühr", - "licensePriceSite": "Preis pro Standort", - "total": "Gesamt", - "licenseContinuePayment": "Weiter zur Zahlung", - "pricingPage": "Preisseite", - "pricingPortal": "Einkaufsportal ansehen", - "licensePricingPage": "Für die aktuellsten Preise und Rabatte, besuchen Sie bitte die ", - "invite": "Einladungen", - "inviteRegenerate": "Einladung neu generieren", - "inviteRegenerateDescription": "Vorherige Einladung widerrufen und neue erstellen", - "inviteRemove": "Einladung entfernen", - "inviteRemoveError": "Einladung konnte nicht entfernt werden", - "inviteRemoveErrorDescription": "Beim Entfernen der Einladung ist ein Fehler aufgetreten.", - "inviteRemoved": "Einladung entfernt", - "inviteRemovedDescription": "Die Einladung für {email} wurde entfernt.", - "inviteQuestionRemove": "Sind Sie sicher, dass Sie die Einladung {email} entfernen möchten?", - "inviteMessageRemove": "Sobald entfernt, wird diese Einladung nicht mehr gültig sein. Sie können den Benutzer später jederzeit erneut einladen.", - "inviteMessageConfirm": "Bitte geben Sie zur Bestätigung die E-Mail-Adresse der Einladung unten ein.", - "inviteQuestionRegenerate": "Sind Sie sicher, dass Sie die Einladung {email} neu generieren möchten? Dies wird die vorherige Einladung widerrufen.", - "inviteRemoveConfirm": "Entfernen der Einladung bestätigen", - "inviteRegenerated": "Einladung neu generiert", - "inviteSent": "Eine neue Einladung wurde an {email} gesendet.", - "inviteSentEmail": "E-Mail-Benachrichtigung an den Benutzer senden", - "inviteGenerate": "Eine neue Einladung wurde für {email} generiert.", - "inviteDuplicateError": "Doppelte Einladung", - "inviteDuplicateErrorDescription": "Eine Einladung für diesen Benutzer existiert bereits.", - "inviteRateLimitError": "Ratenlimit überschritten", - "inviteRateLimitErrorDescription": "Sie haben das Limit von 3 Neugenerierungen pro Stunde überschritten. Bitte versuchen Sie es später erneut.", - "inviteRegenerateError": "Fehler beim Neugenerieren der Einladung", - "inviteRegenerateErrorDescription": "Beim Neugenerieren der Einladung ist ein Fehler aufgetreten.", - "inviteValidityPeriod": "Gültigkeitszeitraum", - "inviteValidityPeriodSelect": "Gültigkeitszeitraum auswählen", - "inviteRegenerateMessage": "Die Einladung wurde neu generiert. Der Benutzer muss den untenstehenden Link aufrufen, um die Einladung anzunehmen.", - "inviteRegenerateButton": "Neu generieren", - "expiresAt": "Läuft ab am", - "accessRoleUnknown": "Unbekannte Rolle", - "placeholder": "Platzhalter", - "userErrorOrgRemove": "Fehler beim Entfernen des Benutzers", - "userErrorOrgRemoveDescription": "Beim Entfernen des Benutzers ist ein Fehler aufgetreten.", - "userOrgRemoved": "Benutzer entfernt", - "userOrgRemovedDescription": "Der Benutzer {email} wurde aus der Organisation entfernt.", - "userQuestionOrgRemove": "Sind Sie sicher, dass Sie {email} aus der Organisation entfernen möchten?", - "userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.", - "userMessageOrgConfirm": "Geben Sie zur Bestätigung den Namen des Benutzers unten ein.", - "userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen", - "userRemoveOrg": "Benutzer aus der Organisation entfernen", - "users": "Benutzer", - "accessRoleMember": "Mitglied", - "accessRoleOwner": "Eigentümer", - "userConfirmed": "Bestätigt", - "idpNameInternal": "Intern", - "emailInvalid": "Ungültige E-Mail-Adresse", - "inviteValidityDuration": "Bitte wählen Sie eine Dauer", - "accessRoleSelectPlease": "Bitte wählen Sie eine Rolle", - "usernameRequired": "Benutzername ist erforderlich", - "idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter", - "idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.", - "accessRoleErrorFetch": "Fehler beim Abrufen der Rollen", - "accessRoleErrorFetchDescription": "Beim Abrufen der Rollen ist ein Fehler aufgetreten", - "idpErrorFetch": "Fehler beim Abrufen der Identitätsanbieter", - "idpErrorFetchDescription": "Beim Abrufen der Identitätsanbieter ist ein Fehler aufgetreten", - "userErrorExists": "Benutzer existiert bereits", - "userErrorExistsDescription": "Dieser Benutzer ist bereits Mitglied der Organisation.", - "inviteError": "Fehler beim Einladen des Benutzers", - "inviteErrorDescription": "Beim Einladen des Benutzers ist ein Fehler aufgetreten", - "userInvited": "Benutzer eingeladen", - "userInvitedDescription": "Der Benutzer wurde erfolgreich eingeladen.", - "userErrorCreate": "Fehler beim Erstellen des Benutzers", - "userErrorCreateDescription": "Beim Erstellen des Benutzers ist ein Fehler aufgetreten", - "userCreated": "Benutzer erstellt", - "userCreatedDescription": "Der Benutzer wurde erfolgreich erstellt.", - "userTypeInternal": "Interner Benutzer", - "userTypeInternalDescription": "Laden Sie einen Benutzer direkt in Ihre Organisation ein.", - "userTypeExternal": "Externer Benutzer", - "userTypeExternalDescription": "Erstellen Sie einen Benutzer mit einem externen Identitätsanbieter.", - "accessUserCreateDescription": "Folgen Sie den Schritten unten, um einen neuen Benutzer zu erstellen", - "userSeeAll": "Alle Benutzer anzeigen", - "userTypeTitle": "Benutzertyp", - "userTypeDescription": "Legen Sie fest, wie Sie den Benutzer erstellen möchten", - "userSettings": "Benutzerinformationen", - "userSettingsDescription": "Geben Sie die Details für den neuen Benutzer ein", - "inviteEmailSent": "Einladungs-E-Mail an Benutzer senden", - "inviteValid": "Gültig für", - "selectDuration": "Dauer auswählen", - "accessRoleSelect": "Rolle auswählen", - "inviteEmailSentDescription": "Eine E-Mail mit dem Zugangslink wurde an den Benutzer gesendet. Er muss den Link aufrufen, um die Einladung anzunehmen.", - "inviteSentDescription": "Der Benutzer wurde eingeladen. Er muss den unten stehenden Link aufrufen, um die Einladung anzunehmen.", - "inviteExpiresIn": "Die Einladung läuft in {days, plural, one {einem Tag} other {# Tagen}} ab.", - "idpTitle": "Allgemeine Informationen", - "idpSelect": "Wählen Sie den Identitätsanbieter für den externen Benutzer", - "idpNotConfigured": "Es sind keine Identitätsanbieter konfiguriert. Bitte konfigurieren Sie einen Identitätsanbieter, bevor Sie externe Benutzer erstellen.", - "usernameUniq": "Dies muss mit dem eindeutigen Benutzernamen übereinstimmen, der im ausgewählten Identitätsanbieter existiert.", - "emailOptional": "E-Mail (Optional)", - "nameOptional": "Name (optional)", - "accessControls": "Zugriffskontrolle", - "userDescription2": "Verwalten Sie die Einstellungen dieses Benutzers", - "accessRoleErrorAdd": "Fehler beim Hinzufügen des Benutzers zur Rolle", - "accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.", - "userSaved": "Benutzer gespeichert", - "userSavedDescription": "Der Benutzer wurde aktualisiert.", - "autoProvisioned": "Automatisch vorgesehen", - "autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter", - "accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann", - "accessControlsSubmit": "Zugriffskontrollen speichern", - "roles": "Rollen", - "accessUsersRoles": "Benutzer & Rollen verwalten", - "accessUsersRolesDescription": "Laden Sie Benutzer ein und fügen Sie sie zu Rollen hinzu, um den Zugriff auf Ihre Organisation zu verwalten", - "key": "Schlüssel", - "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", - "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", - "siteErrorFetchDescription": "Beim Abrufen der Ressource ist ein Fehler aufgetreten", - "targetErrorDuplicate": "Doppeltes Ziel", - "targetErrorDuplicateDescription": "Ein Ziel mit diesen Einstellungen existiert bereits", - "targetWireGuardErrorInvalidIp": "Ungültige Ziel-IP", - "targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Standort-Subnets liegen", - "targetsUpdated": "Ziele aktualisiert", - "targetsUpdatedDescription": "Ziele und Einstellungen erfolgreich aktualisiert", - "targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele", - "targetsErrorUpdateDescription": "Beim Aktualisieren der Ziele ist ein Fehler aufgetreten", - "targetTlsUpdate": "TLS-Einstellungen aktualisiert", - "targetTlsUpdateDescription": "Ihre TLS-Einstellungen wurden erfolgreich aktualisiert", - "targetErrorTlsUpdate": "Fehler beim Aktualisieren der TLS-Einstellungen", - "targetErrorTlsUpdateDescription": "Beim Aktualisieren der TLS-Einstellungen ist ein Fehler aufgetreten", - "proxyUpdated": "Proxy-Einstellungen aktualisiert", - "proxyUpdatedDescription": "Ihre Proxy-Einstellungen wurden erfolgreich aktualisiert", - "proxyErrorUpdate": "Fehler beim Aktualisieren der Proxy-Einstellungen", - "proxyErrorUpdateDescription": "Beim Aktualisieren der Proxy-Einstellungen ist ein Fehler aufgetreten", - "targetAddr": "IP / Hostname", - "targetPort": "Port", - "targetProtocol": "Protokoll", - "targetTlsSettings": "Sicherheitskonfiguration", - "targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource", - "targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen", - "targetTlsSni": "TLS Servername", - "targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.", - "targetTlsSubmit": "Einstellungen speichern", - "targets": "Ziel-Konfiguration", - "targetsDescription": "Richten Sie Ziele ein, um Datenverkehr zu Ihren Backend-Diensten zu leiten", - "targetStickySessions": "Sticky Sessions aktivieren", - "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", - "methodSelect": "Methode auswählen", - "targetSubmit": "Ziel hinzufügen", - "targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.", - "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", - "proxyCustomHeaderDescription": "Der Host-Header, der beim Weiterleiten von Anfragen gesetzt werden soll. Leer lassen, um den Standard zu verwenden.", - "proxyAdditionalSubmit": "Proxy-Einstellungen speichern", - "subnetMaskErrorInvalid": "Ungültige Subnetzmaske. Muss zwischen 0 und 32 liegen.", - "ipAddressErrorInvalidFormat": "Ungültiges IP-Adressformat", - "ipAddressErrorInvalidOctet": "Ungültiges IP-Adress-Oktett", - "path": "Pfad", - "matchPath": "Spielpfad", - "ipAddressRange": "IP-Bereich", - "rulesErrorFetch": "Fehler beim Abrufen der Regeln", - "rulesErrorFetchDescription": "Beim Abrufen der Regeln ist ein Fehler aufgetreten", - "rulesErrorDuplicate": "Doppelte Regel", - "rulesErrorDuplicateDescription": "Eine Regel mit diesen Einstellungen existiert bereits", - "rulesErrorInvalidIpAddressRange": "Ungültiger CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Bitte geben Sie einen gültigen CIDR-Wert ein", - "rulesErrorInvalidUrl": "Ungültiger URL-Pfad", - "rulesErrorInvalidUrlDescription": "Bitte geben Sie einen gültigen URL-Pfad-Wert ein", - "rulesErrorInvalidIpAddress": "Ungültige IP", - "rulesErrorInvalidIpAddressDescription": "Bitte geben Sie eine gültige IP-Adresse ein", - "rulesErrorUpdate": "Fehler beim Aktualisieren der Regeln", - "rulesErrorUpdateDescription": "Beim Aktualisieren der Regeln ist ein Fehler aufgetreten", - "rulesUpdated": "Regeln aktivieren", - "rulesUpdatedDescription": "Die Regelauswertung wurde aktualisiert", - "rulesMatchIpAddressRangeDescription": "Geben Sie eine Adresse im CIDR-Format ein (z.B. 103.21.244.0/22)", - "rulesMatchIpAddress": "Geben Sie eine IP-Adresse ein (z.B. 103.21.244.12)", - "rulesMatchUrl": "Geben Sie einen URL-Pfad oder -Muster ein (z.B. /api/v1/todos oder /api/v1/*)", - "rulesErrorInvalidPriority": "Ungültige Priorität", - "rulesErrorInvalidPriorityDescription": "Bitte geben Sie eine gültige Priorität ein", - "rulesErrorDuplicatePriority": "Doppelte Prioritäten", - "rulesErrorDuplicatePriorityDescription": "Bitte geben Sie eindeutige Prioritäten ein", - "ruleUpdated": "Regeln aktualisiert", - "ruleUpdatedDescription": "Regeln erfolgreich aktualisiert", - "ruleErrorUpdate": "Operation fehlgeschlagen", - "ruleErrorUpdateDescription": "Während des Speichervorgangs ist ein Fehler aufgetreten", - "rulesPriority": "Priorität", - "rulesAction": "Aktion", - "rulesMatchType": "Übereinstimmungstyp", - "value": "Wert", - "rulesAbout": "Über Regeln", - "rulesAboutDescription": "Mit Regeln können Sie den Zugriff auf Ihre Ressource anhand verschiedener Kriterien steuern. Sie können Regeln erstellen, um den Zugriff basierend auf IP-Adresse oder URL-Pfad zu erlauben oder zu verweigern.", - "rulesActions": "Aktionen", - "rulesActionAlwaysAllow": "Immer erlauben: Alle Authentifizierungsmethoden umgehen", - "rulesActionAlwaysDeny": "Immer verweigern: Alle Anfragen blockieren; keine Authentifizierung möglich", - "rulesActionPassToAuth": "Weiterleiten zur Authentifizierung: Erlaubt das Versuchen von Authentifizierungsmethoden", - "rulesMatchCriteria": "Übereinstimmungskriterien", - "rulesMatchCriteriaIpAddress": "Mit einer bestimmten IP-Adresse übereinstimmen", - "rulesMatchCriteriaIpAddressRange": "Mit einem IP-Adressbereich in CIDR-Notation übereinstimmen", - "rulesMatchCriteriaUrl": "Mit einem URL-Pfad oder -Muster übereinstimmen", - "rulesEnable": "Regeln aktivieren", - "rulesEnableDescription": "Regelauswertung für diese Ressource aktivieren oder deaktivieren", - "rulesResource": "Ressourcen-Regelkonfiguration", - "rulesResourceDescription": "Konfigurieren Sie Regeln zur Steuerung des Zugriffs auf Ihre Ressource", - "ruleSubmit": "Regel hinzufügen", - "rulesNoOne": "Keine Regeln. Fügen Sie eine Regel über das Formular hinzu.", - "rulesOrder": "Regeln werden nach aufsteigender Priorität ausgewertet.", - "rulesSubmit": "Regeln speichern", - "resourceErrorCreate": "Fehler beim Erstellen der Ressource", - "resourceErrorCreateDescription": "Beim Erstellen der Ressource ist ein Fehler aufgetreten", - "resourceErrorCreateMessage": "Fehler beim Erstellen der Ressource:", - "resourceErrorCreateMessageDescription": "Ein unerwarteter Fehler ist aufgetreten", - "sitesErrorFetch": "Fehler beim Abrufen der Standorte", - "sitesErrorFetchDescription": "Beim Abrufen der Standorte ist ein Fehler aufgetreten", - "domainsErrorFetch": "Fehler beim Abrufen der Domains", - "domainsErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten", - "none": "Keine", - "unknown": "Unbekannt", - "resources": "Ressourcen", - "resourcesDescription": "Ressourcen sind Proxies zu Anwendungen in Ihrem privaten Netzwerk. Erstellen Sie eine Ressource für jeden HTTP/HTTPS- oder rohen TCP/UDP-Dienst in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Site verbunden sein, um private, sichere Konnektivität über einen verschlüsselten WireGuard-Tunnel zu ermöglichen.", - "resourcesWireGuardConnect": "Sichere Verbindung mit WireGuard-Verschlüsselung", - "resourcesMultipleAuthenticationMethods": "Mehrere Authentifizierungsmethoden konfigurieren", - "resourcesUsersRolesAccess": "Benutzer- und rollenbasierte Zugriffskontrolle", - "resourcesErrorUpdate": "Fehler beim Umschalten der Ressource", - "resourcesErrorUpdateDescription": "Beim Aktualisieren der Ressource ist ein Fehler aufgetreten", - "access": "Zugriff", - "shareLink": "{resource} Freigabe-Link", - "resourceSelect": "Ressource auswählen", - "shareLinks": "Freigabe-Links", - "share": "Teilbare Links", - "shareDescription2": "Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Ablaufzeit des Links bei der Erstellung konfigurieren.", - "shareEasyCreate": "Einfach zu erstellen und zu teilen", - "shareConfigurableExpirationDuration": "Konfigurierbare Ablaufzeit", - "shareSecureAndRevocable": "Sicher und widerrufbar", - "nameMin": "Der Name muss mindestens {len} Zeichen lang sein.", - "nameMax": "Der Name darf nicht länger als {len} Zeichen sein.", - "sitesConfirmCopy": "Bitte bestätigen Sie, dass Sie die Konfiguration kopiert haben.", - "unknownCommand": "Unbekannter Befehl", - "newtErrorFetchReleases": "Fehler beim Abrufen der Release-Informationen: {err}", - "newtErrorFetchLatest": "Fehler beim Abrufen der neuesten Version: {err}", - "newtEndpoint": "Newt-Endpunkt", - "newtId": "Newt-ID", - "newtSecretKey": "Newt-Geheimschlüssel", - "architecture": "Architektur", - "sites": "Standorte", - "siteWgAnyClients": "Verwenden Sie einen beliebigen WireGuard-Client zur Verbindung. Sie müssen Ihre internen Ressourcen über die Peer-IP adressieren.", - "siteWgCompatibleAllClients": "Kompatibel mit allen WireGuard-Clients", - "siteWgManualConfigurationRequired": "Manuelle Konfiguration erforderlich", - "userErrorNotAdminOrOwner": "Benutzer ist kein Administrator oder Eigentümer", - "pangolinSettings": "Einstellungen - Pangolin", - "accessRoleYour": "Ihre {count, plural, =1 {Rolle} other {Rollen}}:", - "accessRoleSelect2": "Wähle eine Rolle", - "accessUserSelect": "Wähle einen Benutzer", - "otpEmailEnter": "E-Mail-Adresse eingeben", - "otpEmailEnterDescription": "Drücken Sie Enter, um eine E-Mail nach der Eingabe im Eingabefeld hinzuzufügen.", - "otpEmailErrorInvalid": "Ungültige E-Mail-Adresse. Platzhalter (*) muss der gesamte lokale Teil sein.", - "otpEmailSmtpRequired": "SMTP erforderlich", - "otpEmailSmtpRequiredDescription": "SMTP muss auf dem Server aktiviert sein, um die Einmal-Passwort-Authentifizierung zu verwenden.", - "otpEmailTitle": "Einmal-Passwörter", - "otpEmailTitleDescription": "E-Mail-basierte Authentifizierung für Ressourcenzugriff erforderlich", - "otpEmailWhitelist": "E-Mail-Whitelist", - "otpEmailWhitelistList": "Zugelassene E-Mails", - "otpEmailWhitelistListDescription": "Nur Benutzer mit diesen E-Mail-Adressen können auf diese Ressource zugreifen. Sie werden aufgefordert, ein an ihre E-Mail gesendetes Einmal-Passwort einzugeben. Platzhalter (*@example.com) können verwendet werden, um E-Mail-Adressen einer Domain zuzulassen.", - "otpEmailWhitelistSave": "Whitelist speichern", - "passwordAdd": "Passwort hinzufügen", - "passwordRemove": "Passwort entfernen", - "pincodeAdd": "PIN-Code hinzufügen", - "pincodeRemove": "PIN-Code entfernen", - "resourceAuthMethods": "Authentifizierungsmethoden", - "resourceAuthMethodsDescriptions": "Ermöglichen Sie den Zugriff auf die Ressource über zusätzliche Authentifizierungsmethoden", - "resourceAuthSettingsSave": "Erfolgreich gespeichert", - "resourceAuthSettingsSaveDescription": "Authentifizierungseinstellungen wurden gespeichert", - "resourceErrorAuthFetch": "Fehler beim Abrufen der Daten", - "resourceErrorAuthFetchDescription": "Beim Abrufen der Daten ist ein Fehler aufgetreten", - "resourceErrorPasswordRemove": "Fehler beim Entfernen des Ressourcenpassworts", - "resourceErrorPasswordRemoveDescription": "Beim Entfernen des Ressourcenpassworts ist ein Fehler aufgetreten", - "resourceErrorPasswordSetup": "Fehler beim Einrichten des Ressourcenpassworts", - "resourceErrorPasswordSetupDescription": "Beim Einrichten des Ressourcenpassworts ist ein Fehler aufgetreten", - "resourceErrorPincodeRemove": "Fehler beim Entfernen des Ressourcen-PIN-Codes", - "resourceErrorPincodeRemoveDescription": "Beim Entfernen des Ressourcen-PIN-Codes ist ein Fehler aufgetreten", - "resourceErrorPincodeSetup": "Fehler beim Einrichten des Ressourcen-PIN-Codes", - "resourceErrorPincodeSetupDescription": "Beim Einrichten des Ressourcen-PIN-Codes ist ein Fehler aufgetreten", - "resourceErrorUsersRolesSave": "Fehler beim Speichern der Rollen", - "resourceErrorUsersRolesSaveDescription": "Beim Speichern der Rollen ist ein Fehler aufgetreten", - "resourceErrorWhitelistSave": "Fehler beim Speichern der Whitelist", - "resourceErrorWhitelistSaveDescription": "Beim Speichern der Whitelist ist ein Fehler aufgetreten", - "resourcePasswordSubmit": "Passwortschutz aktivieren", - "resourcePasswordProtection": "Passwortschutz {status}", - "resourcePasswordRemove": "Ressourcenpasswort entfernt", - "resourcePasswordRemoveDescription": "Das Ressourcenpasswort wurde erfolgreich entfernt", - "resourcePasswordSetup": "Ressourcenpasswort festgelegt", - "resourcePasswordSetupDescription": "Das Ressourcenpasswort wurde erfolgreich festgelegt", - "resourcePasswordSetupTitle": "Passwort festlegen", - "resourcePasswordSetupTitleDescription": "Legen Sie ein Passwort fest, um diese Ressource zu schützen", - "resourcePincode": "PIN-Code", - "resourcePincodeSubmit": "PIN-Code-Schutz aktivieren", - "resourcePincodeProtection": "PIN-Code-Schutz {status}", - "resourcePincodeRemove": "Ressourcen-PIN-Code entfernt", - "resourcePincodeRemoveDescription": "Der Ressourcen-PIN-Code wurde erfolgreich entfernt", - "resourcePincodeSetup": "Ressourcen-PIN-Code festgelegt", - "resourcePincodeSetupDescription": "Der Ressourcen-PIN-Code wurde erfolgreich festgelegt", - "resourcePincodeSetupTitle": "PIN-Code festlegen", - "resourcePincodeSetupTitleDescription": "Legen Sie einen PIN-Code fest, um diese Ressource zu schützen", - "resourceRoleDescription": "Administratoren haben immer Zugriff auf diese Ressource.", - "resourceUsersRoles": "Benutzer & Rollen", - "resourceUsersRolesDescription": "Konfigurieren Sie, welche Benutzer und Rollen diese Ressource besuchen können", - "resourceUsersRolesSubmit": "Benutzer & Rollen speichern", - "resourceWhitelistSave": "Erfolgreich gespeichert", - "resourceWhitelistSaveDescription": "Whitelist-Einstellungen wurden gespeichert", - "ssoUse": "Plattform SSO verwenden", - "ssoUseDescription": "Bestehende Benutzer müssen sich nur einmal für alle Ressourcen anmelden, bei denen dies aktiviert ist.", - "proxyErrorInvalidPort": "Ungültige Portnummer", - "subdomainErrorInvalid": "Ungültige Subdomain", - "domainErrorFetch": "Fehler beim Abrufen der Domains", - "domainErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten", - "resourceErrorUpdate": "Ressource konnte nicht aktualisiert werden", - "resourceErrorUpdateDescription": "Beim Aktualisieren der Ressource ist ein Fehler aufgetreten", - "resourceUpdated": "Ressource aktualisiert", - "resourceUpdatedDescription": "Die Ressource wurde erfolgreich aktualisiert", - "resourceErrorTransfer": "Ressource konnte nicht übertragen werden", - "resourceErrorTransferDescription": "Beim Übertragen der Ressource ist ein Fehler aufgetreten", - "resourceTransferred": "Ressource übertragen", - "resourceTransferredDescription": "Die Ressource wurde erfolgreich übertragen", - "resourceErrorToggle": "Ressource konnte nicht umgeschaltet werden", - "resourceErrorToggleDescription": "Beim Aktualisieren der Ressource ist ein Fehler aufgetreten", - "resourceVisibilityTitle": "Sichtbarkeit", - "resourceVisibilityTitleDescription": "Ressourcensichtbarkeit vollständig aktivieren oder deaktivieren", - "resourceGeneral": "Allgemeine Einstellungen", - "resourceGeneralDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diese Ressource", - "resourceEnable": "Ressource aktivieren", - "resourceTransfer": "Ressource übertragen", - "resourceTransferDescription": "Diese Ressource auf einen anderen Standort übertragen", - "resourceTransferSubmit": "Ressource übertragen", - "siteDestination": "Zielort", - "searchSites": "Standorte durchsuchen", - "accessRoleCreate": "Rolle erstellen", - "accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.", - "accessRoleCreateSubmit": "Rolle erstellen", - "accessRoleCreated": "Rolle erstellt", - "accessRoleCreatedDescription": "Die Rolle wurde erfolgreich erstellt.", - "accessRoleErrorCreate": "Fehler beim Erstellen der Rolle", - "accessRoleErrorCreateDescription": "Beim Erstellen der Rolle ist ein Fehler aufgetreten.", - "accessRoleErrorNewRequired": "Neue Rolle ist erforderlich", - "accessRoleErrorRemove": "Fehler beim Entfernen der Rolle", - "accessRoleErrorRemoveDescription": "Beim Entfernen der Rolle ist ein Fehler aufgetreten.", - "accessRoleName": "Rollenname", - "accessRoleQuestionRemove": "Sie sind dabei, die Rolle {name} zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.", - "accessRoleRemove": "Rolle entfernen", - "accessRoleRemoveDescription": "Eine Rolle aus der Organisation entfernen", - "accessRoleRemoveSubmit": "Rolle entfernen", - "accessRoleRemoved": "Rolle entfernt", - "accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.", - "accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.", - "manage": "Verwalten", - "sitesNotFound": "Keine Standorte gefunden.", - "pangolinServerAdmin": "Server-Admin - Pangolin", - "licenseTierProfessional": "Professional Lizenz", - "licenseTierEnterprise": "Enterprise Lizenz", - "licenseTierPersonal": "Personal License", - "licensed": "Lizenziert", - "yes": "Ja", - "no": "Nein", - "sitesAdditional": "Zusätzliche Standorte", - "licenseKeys": "Lizenzschlüssel", - "sitestCountDecrease": "Anzahl der Standorte verringern", - "sitestCountIncrease": "Anzahl der Standorte erhöhen", - "idpManage": "Identitätsanbieter verwalten", - "idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten", - "idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Sind Sie sicher, dass Sie den Identitätsanbieter {name} dauerhaft löschen möchten?", - "idpMessageRemove": "Dies wird den Identitätsanbieter und alle zugehörigen Konfigurationen entfernen. Benutzer, die sich über diesen Anbieter authentifizieren, können sich nicht mehr anmelden.", - "idpMessageConfirm": "Bitte geben Sie zur Bestätigung den Namen des Identitätsanbieters unten ein.", - "idpConfirmDelete": "Löschen des Identitätsanbieters bestätigen", - "idpDelete": "Identitätsanbieter löschen", - "idp": "Identitätsanbieter", - "idpSearch": "Identitätsanbieter suchen...", - "idpAdd": "Identitätsanbieter hinzufügen", - "idpClientIdRequired": "Client-ID ist erforderlich.", - "idpClientSecretRequired": "Client-Secret ist erforderlich.", - "idpErrorAuthUrlInvalid": "Auth-URL muss eine gültige URL sein.", - "idpErrorTokenUrlInvalid": "Token-URL muss eine gültige URL sein.", - "idpPathRequired": "Identifikationspfad ist erforderlich.", - "idpScopeRequired": "Scopes sind erforderlich.", - "idpOidcDescription": "Konfigurieren Sie einen OpenID Connect Identitätsanbieter", - "idpCreatedDescription": "Identitätsanbieter erfolgreich erstellt", - "idpCreate": "Identitätsanbieter erstellen", - "idpCreateDescription": "Konfigurieren Sie einen neuen Identitätsanbieter für die Benutzerauthentifizierung", - "idpSeeAll": "Alle Identitätsanbieter anzeigen", - "idpSettingsDescription": "Konfigurieren Sie die grundlegenden Informationen für Ihren Identitätsanbieter", - "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": "EE", - "idpType": "Anbietertyp", - "idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten", - "idpOidcConfigure": "OAuth2/OIDC Konfiguration", - "idpOidcConfigureDescription": "Konfigurieren Sie die OAuth2/OIDC Anbieter-Endpunkte und Anmeldeinformationen", - "idpClientId": "Client-ID", - "idpClientIdDescription": "Die OAuth2 Client-ID von Ihrem Identitätsanbieter", - "idpClientSecret": "Client-Secret", - "idpClientSecretDescription": "Das OAuth2 Client-Secret von Ihrem Identitätsanbieter", - "idpAuthUrl": "Autorisierungs-URL", - "idpAuthUrlDescription": "Die OAuth2 Autorisierungs-Endpunkt-URL", - "idpTokenUrl": "Token-URL", - "idpTokenUrlDescription": "Die OAuth2 Token-Endpunkt-URL", - "idpOidcConfigureAlert": "Wichtige Information", - "idpOidcConfigureAlertDescription": "Nach dem Erstellen des Identitätsanbieters müssen Sie die Callback-URL in den Einstellungen Ihres Identitätsanbieters konfigurieren. Die Callback-URL wird nach erfolgreicher Erstellung bereitgestellt.", - "idpToken": "Token-Konfiguration", - "idpTokenDescription": "Konfigurieren Sie, wie Benutzerinformationen aus dem ID-Token extrahiert werden", - "idpJmespathAbout": "Über JMESPath", - "idpJmespathAboutDescription": "Die unten stehenden Pfade verwenden JMESPath-Syntax, um Werte aus dem ID-Token zu extrahieren.", - "idpJmespathAboutDescriptionLink": "Mehr über JMESPath erfahren", - "idpJmespathLabel": "Identifikationspfad", - "idpJmespathLabelDescription": "Der JMESPath zum Benutzeridentifikator im ID-Token", - "idpJmespathEmailPathOptional": "E-Mail-Pfad (Optional)", - "idpJmespathEmailPathOptionalDescription": "Der JMESPath zur E-Mail-Adresse des Benutzers im ID-Token", - "idpJmespathNamePathOptional": "Namenspfad (Optional)", - "idpJmespathNamePathOptionalDescription": "Der JMESPath zum Namen des Benutzers im ID-Token", - "idpOidcConfigureScopes": "Bereiche", - "idpOidcConfigureScopesDescription": "Durch Leerzeichen getrennte Liste der anzufordernden OAuth2-Scopes", - "idpSubmit": "Identitätsanbieter erstellen", - "orgPolicies": "Organisationsrichtlinien", - "idpSettings": "{idpName} Einstellungen", - "idpCreateSettingsDescription": "Konfigurieren Sie die Einstellungen für Ihren Identitätsanbieter", - "roleMapping": "Rollenzuordnung", - "orgMapping": "Organisationszuordnung", - "orgPoliciesSearch": "Organisationsrichtlinien suchen...", - "orgPoliciesAdd": "Organisationsrichtlinie hinzufügen", - "orgRequired": "Organisation ist erforderlich", - "error": "Fehler", - "success": "Erfolg", - "orgPolicyAddedDescription": "Richtlinie erfolgreich hinzugefügt", - "orgPolicyUpdatedDescription": "Richtlinie erfolgreich aktualisiert", - "orgPolicyDeletedDescription": "Richtlinie erfolgreich gelöscht", - "defaultMappingsUpdatedDescription": "Standardzuordnungen erfolgreich aktualisiert", - "orgPoliciesAbout": "Über Organisationsrichtlinien", - "orgPoliciesAboutDescription": "Organisationsrichtlinien werden verwendet, um den Zugriff auf Organisationen basierend auf dem ID-Token des Benutzers zu steuern. Sie können JMESPath-Ausdrücke angeben, um Rollen- und Organisationsinformationen aus dem ID-Token zu extrahieren. Weitere Informationen finden Sie in", - "orgPoliciesAboutDescriptionLink": "der Dokumentation", - "defaultMappingsOptional": "Standardzuordnungen (Optional)", - "defaultMappingsOptionalDescription": "Die Standardzuordnungen werden verwendet, wenn keine Organisationsrichtlinie für eine Organisation definiert ist. Sie können hier die Standard-Rollen- und Organisationszuordnungen festlegen.", - "defaultMappingsRole": "Standard-Rollenzuordnung", - "defaultMappingsRoleDescription": "JMESPath zur Extraktion von Rolleninformationen aus dem ID-Token. Das Ergebnis dieses Ausdrucks muss den Rollennamen als String zurückgeben, wie er in der Organisation definiert ist.", - "defaultMappingsOrg": "Standard-Organisationszuordnung", - "defaultMappingsOrgDescription": "JMESPath zur Extraktion von Organisationsinformationen aus dem ID-Token. Dieser Ausdruck muss die Organisations-ID oder true zurückgeben, damit der Benutzer Zugriff auf die Organisation erhält.", - "defaultMappingsSubmit": "Standardzuordnungen speichern", - "orgPoliciesEdit": "Organisationsrichtlinie bearbeiten", - "org": "Organisation", - "orgSelect": "Organisation auswählen", - "orgSearch": "Organisation suchen", - "orgNotFound": "Keine Organisation gefunden.", - "roleMappingPathOptional": "Rollenzuordnungspfad (Optional)", - "orgMappingPathOptional": "Organisationszuordnungspfad (Optional)", - "orgPolicyUpdate": "Richtlinie aktualisieren", - "orgPolicyAdd": "Richtlinie hinzufügen", - "orgPolicyConfig": "Zugriff für eine Organisation konfigurieren", - "idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert", - "redirectUrl": "Weiterleitungs-URL", - "redirectUrlAbout": "Über die Weiterleitungs-URL", - "redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung weitergeleitet werden. Sie müssen diese URL in den Einstellungen Ihres Identitätsanbieters konfigurieren.", - "pangolinAuth": "Authentifizierung - Pangolin", - "verificationCodeLengthRequirements": "Ihr Verifizierungscode muss 8 Zeichen lang sein.", - "errorOccurred": "Ein Fehler ist aufgetreten", - "emailErrorVerify": "E-Mail konnte nicht verifiziert werden:", - "emailVerified": "E-Mail erfolgreich verifiziert! Sie werden weitergeleitet...", - "verificationCodeErrorResend": "Verifizierungscode konnte nicht erneut gesendet werden:", - "verificationCodeResend": "Verifizierungscode erneut gesendet", - "verificationCodeResendDescription": "Wir haben einen neuen Verifizierungscode an Ihre E-Mail-Adresse gesendet. Bitte prüfen Sie Ihren Posteingang.", - "emailVerify": "E-Mail verifizieren", - "emailVerifyDescription": "Geben Sie den an Ihre E-Mail-Adresse gesendeten Verifizierungscode ein.", - "verificationCode": "Verifizierungscode", - "verificationCodeEmailSent": "Wir haben einen Verifizierungscode an Ihre E-Mail-Adresse gesendet.", - "submit": "Absenden", - "emailVerifyResendProgress": "Wird erneut gesendet...", - "emailVerifyResend": "Keinen Code erhalten? Hier klicken zum erneuten Senden", - "passwordNotMatch": "Passwörter stimmen nicht überein", - "signupError": "Beim Registrieren ist ein Fehler aufgetreten", - "pangolinLogoAlt": "Pangolin-Logo", - "inviteAlready": "Sieht aus, als wären Sie eingeladen worden!", - "inviteAlreadyDescription": "Um die Einladung anzunehmen, müssen Sie sich einloggen oder ein Konto erstellen.", - "signupQuestion": "Haben Sie bereits ein Konto?", - "login": "Anmelden", - "resourceNotFound": "Ressource nicht gefunden", - "resourceNotFoundDescription": "Die Ressource, auf die Sie zugreifen möchten, existiert nicht.", - "pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein", - "pincodeRequirementsChars": "PIN darf nur Zahlen enthalten", - "passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein", - "passwordRequirementsTitle": "Passwortanforderungen:", - "passwordRequirementLength": "Mindestens 8 Zeichen lang", - "passwordRequirementUppercase": "Mindestens ein Großbuchstabe", - "passwordRequirementLowercase": "Mindestens ein Kleinbuchstabe", - "passwordRequirementNumber": "Mindestens eine Zahl", - "passwordRequirementSpecial": "Mindestens ein Sonderzeichen", - "passwordRequirementsMet": "✓ Passwort erfüllt alle Anforderungen", - "passwordStrength": "Passwortstärke", - "passwordStrengthWeak": "Schwach", - "passwordStrengthMedium": "Mittel", - "passwordStrengthStrong": "Stark", - "passwordRequirements": "Anforderungen:", - "passwordRequirementLengthText": "8+ Zeichen", - "passwordRequirementUppercaseText": "Großbuchstabe (A-Z)", - "passwordRequirementLowercaseText": "Kleinbuchstabe (a-z)", - "passwordRequirementNumberText": "Zahl (0-9)", - "passwordRequirementSpecialText": "Sonderzeichen (!@#$%...)", - "passwordsDoNotMatch": "Passwörter stimmen nicht überein", - "otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein", - "otpEmailSent": "OTP gesendet", - "otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet", - "otpEmailErrorAuthenticate": "Authentifizierung per E-Mail fehlgeschlagen", - "pincodeErrorAuthenticate": "Authentifizierung per PIN fehlgeschlagen", - "passwordErrorAuthenticate": "Authentifizierung per Passwort fehlgeschlagen", - "poweredBy": "Bereitgestellt von", - "authenticationRequired": "Authentifizierung erforderlich", - "authenticationMethodChoose": "Wählen Sie Ihre bevorzugte Methode für den Zugriff auf {name}", - "authenticationRequest": "Sie müssen sich authentifizieren, um auf {name} zuzugreifen", - "user": "Benutzer", - "pincodeInput": "6-stelliger PIN-Code", - "pincodeSubmit": "Mit PIN anmelden", - "passwordSubmit": "Mit Passwort anmelden", - "otpEmailDescription": "Ein Einmalcode wird an diese E-Mail gesendet.", - "otpEmailSend": "Einmalcode senden", - "otpEmail": "Einmalpasswort (OTP)", - "otpEmailSubmit": "OTP absenden", - "backToEmail": "Zurück zur E-Mail", - "noSupportKey": "Server läuft ohne Unterstützungsschlüssel. Erwägen Sie die Unterstützung des Projekts!", - "accessDenied": "Zugriff verweigert", - "accessDeniedDescription": "Sie haben keine Berechtigung, auf diese Ressource zuzugreifen. Falls dies ein Fehler ist, kontaktieren Sie bitte den Administrator.", - "accessTokenError": "Fehler beim Prüfen des Zugriffstokens", - "accessGranted": "Zugriff gewährt", - "accessUrlInvalid": "Zugriffs-URL ungültig", - "accessGrantedDescription": "Ihnen wurde Zugriff auf diese Ressource gewährt. Sie werden weitergeleitet...", - "accessUrlInvalidDescription": "Diese geteilte Zugriffs-URL ist ungültig. Bitte kontaktieren Sie den Ressourceneigentümer für eine neue URL.", - "tokenInvalid": "Ungültiger Token", - "pincodeInvalid": "Ungültiger Code", - "passwordErrorRequestReset": "Zurücksetzung konnte nicht angefordert werden:", - "passwordErrorReset": "Passwort konnte nicht zurückgesetzt werden:", - "passwordResetSuccess": "Passwort erfolgreich zurückgesetzt! Zurück zur Anmeldung...", - "passwordReset": "Passwort zurücksetzen", - "passwordResetDescription": "Folgen Sie den Schritten, um Ihr Passwort zurückzusetzen", - "passwordResetSent": "Wir senden einen Code zum Zurücksetzen des Passworts an diese E-Mail-Adresse.", - "passwordResetCode": "Reset-Code", - "passwordResetCodeDescription": "Prüfen Sie Ihre E-Mail für den Reset-Code.", - "passwordNew": "Neues Passwort", - "passwordNewConfirm": "Neues Passwort bestätigen", - "pincodeAuth": "Authentifizierungscode", - "pincodeSubmit2": "Code absenden", - "passwordResetSubmit": "Zurücksetzung anfordern", - "passwordBack": "Zurück zum Passwort", - "loginBack": "Zurück zur Anmeldung", - "signup": "Registrieren", - "loginStart": "Melden Sie sich an, um zu beginnen", - "idpOidcTokenValidating": "OIDC-Token wird validiert", - "idpOidcTokenResponse": "OIDC-Token-Antwort validieren", - "idpErrorOidcTokenValidating": "Fehler beim Validieren des OIDC-Tokens", - "idpConnectingTo": "Verbindung zu {name} wird hergestellt", - "idpConnectingToDescription": "Ihre Identität wird überprüft", - "idpConnectingToProcess": "Verbindung wird hergestellt...", - "idpConnectingToFinished": "Verbunden", - "idpErrorConnectingTo": "Es gab ein Problem bei der Verbindung zu {name}. Bitte kontaktieren Sie Ihren Administrator.", - "idpErrorNotFound": "IdP nicht gefunden", - "inviteInvalid": "Ungültige Einladung", - "inviteInvalidDescription": "Der Einladungslink ist ungültig.", - "inviteErrorWrongUser": "Einladung ist nicht für diesen Benutzer", - "inviteErrorUserNotExists": "Benutzer existiert nicht. Bitte erstelle zuerst ein Konto.", - "inviteErrorLoginRequired": "Du musst angemeldet sein, um eine Einladung anzunehmen", - "inviteErrorExpired": "Die Einladung ist möglicherweise abgelaufen", - "inviteErrorRevoked": "Die Einladung wurde möglicherweise widerrufen", - "inviteErrorTypo": "Es könnte ein Tippfehler im Einladungslink sein", - "pangolinSetup": "Einrichtung - Pangolin", - "orgNameRequired": "Organisationsname ist erforderlich", - "orgIdRequired": "Organisations-ID ist erforderlich", - "orgErrorCreate": "Beim Erstellen der Organisation ist ein Fehler aufgetreten", - "pageNotFound": "Seite nicht gefunden", - "pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.", - "overview": "Übersicht", - "home": "Startseite", - "accessControl": "Zugriffskontrolle", - "settings": "Einstellungen", - "usersAll": "Alle Benutzer", - "license": "Lizenz", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "Keine Ergebnisse gefunden.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Eingegebene Tags", - "tagsEnteredDescription": "Dies sind die von Ihnen eingegebenen Tags.", - "tagsWarnCannotBeLessThanZero": "maxTags und minTags können nicht kleiner als 0 sein", - "tagsWarnNotAllowedAutocompleteOptions": "Tag ist laut Autovervollständigungsoptionen nicht erlaubt", - "tagsWarnInvalid": "Ungültiger Tag laut validateTag", - "tagWarnTooShort": "Tag {tagText} ist zu kurz", - "tagWarnTooLong": "Tag {tagText} ist zu lang", - "tagsWarnReachedMaxNumber": "Maximale Anzahl erlaubter Tags erreicht", - "tagWarnDuplicate": "Doppelter Tag {tagText} nicht hinzugefügt", - "supportKeyInvalid": "Ungültiger Schlüssel", - "supportKeyInvalidDescription": "Ihr Unterstützer-Schlüssel ist ungültig.", - "supportKeyValid": "Gültiger Schlüssel", - "supportKeyValidDescription": "Ihr Unterstützer-Schlüssel wurde validiert. Danke für Ihre Unterstützung!", - "supportKeyErrorValidationDescription": "Unterstützer-Schlüssel konnte nicht validiert werden.", - "supportKey": "Unterstütze die Entwicklung und adoptiere ein Pangolin!", - "supportKeyDescription": "Kaufen Sie einen Unterstützer-Schlüssel, um uns bei der Weiterentwicklung von Pangolin für die Community zu helfen. Ihr Beitrag ermöglicht es uns, mehr Zeit in die Wartung und neue Funktionen für alle zu investieren. Wir werden dies nie für Paywalls nutzen. Dies ist unabhängig von der Commercial Edition.", - "supportKeyPet": "Sie können auch Ihr eigenes Pangolin-Haustier adoptieren und kennenlernen!", - "supportKeyPurchase": "Zahlungen werden über GitHub abgewickelt. Danach können Sie Ihren Schlüssel auf", - "supportKeyPurchaseLink": "unserer Website", - "supportKeyPurchase2": "abrufen und hier einlösen.", - "supportKeyLearnMore": "Mehr erfahren.", - "supportKeyOptions": "Bitte wählen Sie die Option, die am besten zu Ihnen passt.", - "supportKetOptionFull": "Voller Unterstützer", - "forWholeServer": "Für den gesamten Server", - "lifetimePurchase": "Lebenslanger Kauf", - "supporterStatus": "Unterstützer-Status", - "buy": "Kaufen", - "supportKeyOptionLimited": "Eingeschränkter Unterstützer", - "forFiveUsers": "Für 5 oder weniger Benutzer", - "supportKeyRedeem": "Unterstützer-Schlüssel einlösen", - "supportKeyHideSevenDays": "7 Tage ausblenden", - "supportKeyEnter": "Unterstützer-Schlüssel eingeben", - "supportKeyEnterDescription": "Treffen Sie Ihr eigenes Pangolin-Haustier!", - "githubUsername": "GitHub Benutzername", - "supportKeyInput": "Unterstützer-Schlüssel", - "supportKeyBuy": "Unterstützer-Schlüssel kaufen", - "logoutError": "Fehler beim Abmelden", - "signingAs": "Angemeldet als", - "serverAdmin": "Server-Administrator", - "managedSelfhosted": "Verwaltetes Selbsthosted", - "otpEnable": "Zwei-Faktor aktivieren", - "otpDisable": "Zwei-Faktor deaktivieren", - "logout": "Abmelden", - "licenseTierProfessionalRequired": "Professional Edition erforderlich", - "licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.", - "actionGetOrg": "Organisation abrufen", - "updateOrgUser": "Org Benutzer aktualisieren", - "createOrgUser": "Org Benutzer erstellen", - "actionUpdateOrg": "Organisation aktualisieren", - "actionUpdateUser": "Benutzer aktualisieren", - "actionGetUser": "Benutzer abrufen", - "actionGetOrgUser": "Organisationsbenutzer abrufen", - "actionListOrgDomains": "Organisationsdomänen auflisten", - "actionCreateSite": "Standort erstellen", - "actionDeleteSite": "Standort löschen", - "actionGetSite": "Standort abrufen", - "actionListSites": "Standorte auflisten", - "actionApplyBlueprint": "Blaupause anwenden", - "setupToken": "Setup-Token", - "setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.", - "setupTokenRequired": "Setup-Token ist erforderlich", - "actionUpdateSite": "Standorte aktualisieren", - "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", - "actionCreateResource": "Ressource erstellen", - "actionDeleteResource": "Ressource löschen", - "actionGetResource": "Ressource abrufen", - "actionListResource": "Ressourcen auflisten", - "actionUpdateResource": "Ressource aktualisieren", - "actionListResourceUsers": "Ressourcenbenutzer auflisten", - "actionSetResourceUsers": "Ressourcenbenutzer festlegen", - "actionSetAllowedResourceRoles": "Erlaubte Ressourcenrollen festlegen", - "actionListAllowedResourceRoles": "Erlaubte Ressourcenrollen auflisten", - "actionSetResourcePassword": "Ressourcenpasswort festlegen", - "actionSetResourcePincode": "Ressourcen-PIN festlegen", - "actionSetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist festlegen", - "actionGetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist abrufen", - "actionCreateTarget": "Ziel erstellen", - "actionDeleteTarget": "Ziel löschen", - "actionGetTarget": "Ziel abrufen", - "actionListTargets": "Ziele auflisten", - "actionUpdateTarget": "Ziel aktualisieren", - "actionCreateRole": "Rolle erstellen", - "actionDeleteRole": "Rolle löschen", - "actionGetRole": "Rolle abrufen", - "actionListRole": "Rollen auflisten", - "actionUpdateRole": "Rolle aktualisieren", - "actionListAllowedRoleResources": "Erlaubte Rollenressourcen auflisten", - "actionInviteUser": "Benutzer einladen", - "actionRemoveUser": "Benutzer entfernen", - "actionListUsers": "Benutzer auflisten", - "actionAddUserRole": "Benutzerrolle hinzufügen", - "actionGenerateAccessToken": "Zugriffstoken generieren", - "actionDeleteAccessToken": "Zugriffstoken löschen", - "actionListAccessTokens": "Zugriffstoken auflisten", - "actionCreateResourceRule": "Ressourcenregel erstellen", - "actionDeleteResourceRule": "Ressourcenregel löschen", - "actionListResourceRules": "Ressourcenregeln auflisten", - "actionUpdateResourceRule": "Ressourcenregel aktualisieren", - "actionListOrgs": "Organisationen auflisten", - "actionCheckOrgId": "ID prüfen", - "actionCreateOrg": "Organisation erstellen", - "actionDeleteOrg": "Organisation löschen", - "actionListApiKeys": "API-Schlüssel auflisten", - "actionListApiKeyActions": "API-Schlüsselaktionen auflisten", - "actionSetApiKeyActions": "Erlaubte API-Schlüsselaktionen festlegen", - "actionCreateApiKey": "API-Schlüssel erstellen", - "actionDeleteApiKey": "API-Schlüssel löschen", - "actionCreateIdp": "IDP erstellen", - "actionUpdateIdp": "IDP aktualisieren", - "actionDeleteIdp": "IDP löschen", - "actionListIdps": "IDP auflisten", - "actionGetIdp": "IDP abrufen", - "actionCreateIdpOrg": "IDP-Organisationsrichtlinie erstellen", - "actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen", - "actionListIdpOrgs": "IDP-Organisationen auflisten", - "actionUpdateIdpOrg": "IDP-Organisation aktualisieren", - "actionCreateClient": "Kunde erstellen", - "actionDeleteClient": "Kunde löschen", - "actionUpdateClient": "Kunde aktualisieren", - "actionListClients": "Kunden auflisten", - "actionGetClient": "Kunde holen", - "actionCreateSiteResource": "Site-Ressource erstellen", - "actionDeleteSiteResource": "Site-Ressource löschen", - "actionGetSiteResource": "Site-Ressource abrufen", - "actionListSiteResources": "Site-Ressourcen auflisten", - "actionUpdateSiteResource": "Site-Ressource aktualisieren", - "actionListInvitations": "Einladungen auflisten", - "noneSelected": "Keine ausgewählt", - "orgNotFound2": "Keine Organisationen gefunden.", - "searchProgress": "Suche...", - "create": "Erstellen", - "orgs": "Organisationen", - "loginError": "Beim Anmelden ist ein Fehler aufgetreten", - "passwordForgot": "Passwort vergessen?", - "otpAuth": "Zwei-Faktor-Authentifizierung", - "otpAuthDescription": "Geben Sie den Code aus Ihrer Authenticator-App oder einen Ihrer einmaligen Backup-Codes ein.", - "otpAuthSubmit": "Code absenden", - "idpContinue": "Oder weiter mit", - "otpAuthBack": "Zurück zur Anmeldung", - "navbar": "Navigationsmenü", - "navbarDescription": "Hauptnavigationsmenü für die Anwendung", - "navbarDocsLink": "Dokumentation", - "otpErrorEnable": "2FA konnte nicht aktiviert werden", - "otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten", - "otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein", - "otpSetupCheckCodeRetry": "Ungültiger Code. Bitte versuchen Sie es erneut.", - "otpSetup": "Zwei-Faktor-Authentifizierung aktivieren", - "otpSetupDescription": "Sichern Sie Ihr Konto mit einer zusätzlichen Schutzebene", - "otpSetupScanQr": "Scannen Sie diesen QR-Code mit Ihrer Authenticator-App oder geben Sie den Geheimschlüssel manuell ein:", - "otpSetupSecretCode": "Authenticator-Code", - "otpSetupSuccess": "Zwei-Faktor-Authentifizierung aktiviert", - "otpSetupSuccessStoreBackupCodes": "Ihr Konto ist jetzt sicherer. Vergessen Sie nicht, Ihre Backup-Codes zu speichern.", - "otpErrorDisable": "2FA konnte nicht deaktiviert werden", - "otpErrorDisableDescription": "Beim Deaktivieren der 2FA ist ein Fehler aufgetreten", - "otpRemove": "Zwei-Faktor-Authentifizierung deaktivieren", - "otpRemoveDescription": "Deaktivieren Sie die Zwei-Faktor-Authentifizierung für Ihr Konto", - "otpRemoveSuccess": "Zwei-Faktor-Authentifizierung deaktiviert", - "otpRemoveSuccessMessage": "Die Zwei-Faktor-Authentifizierung wurde für Ihr Konto deaktiviert. Sie können sie jederzeit wieder aktivieren.", - "otpRemoveSubmit": "2FA deaktivieren", - "paginator": "Seite {current} von {last}", - "paginatorToFirst": "Zur ersten Seite", - "paginatorToPrevious": "Zur vorherigen Seite", - "paginatorToNext": "Zur nächsten Seite", - "paginatorToLast": "Zur letzten Seite", - "copyText": "Text kopieren", - "copyTextFailed": "Text konnte nicht kopiert werden: ", - "copyTextClipboard": "In die Zwischenablage kopieren", - "inviteErrorInvalidConfirmation": "Ungültige Bestätigung", - "passwordRequired": "Passwort ist erforderlich", - "allowAll": "Alle erlauben", - "permissionsAllowAll": "Alle Berechtigungen erlauben", - "githubUsernameRequired": "GitHub-Benutzername ist erforderlich", - "supportKeyRequired": "Unterstützer-Schlüssel ist erforderlich", - "passwordRequirementsChars": "Das Passwort muss mindestens 8 Zeichen lang sein", - "language": "Sprache", - "verificationCodeRequired": "Code ist erforderlich", - "userErrorNoUpdate": "Kein Benutzer zum Aktualisieren", - "siteErrorNoUpdate": "Keine Standorte zum Aktualisieren", - "resourceErrorNoUpdate": "Keine Ressource zum Aktualisieren", - "authErrorNoUpdate": "Keine Auth-Informationen zum Aktualisieren", - "orgErrorNoUpdate": "Keine Organisation zum Aktualisieren", - "orgErrorNoProvided": "Keine Organisation angegeben", - "apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren", - "sidebarOverview": "Übersicht", - "sidebarHome": "Zuhause", - "sidebarSites": "Standorte", - "sidebarResources": "Ressourcen", - "sidebarAccessControl": "Zugriffskontrolle", - "sidebarUsers": "Benutzer", - "sidebarInvitations": "Einladungen", - "sidebarRoles": "Rollen", - "sidebarShareableLinks": "Teilbare Links", - "sidebarApiKeys": "API-Schlüssel", - "sidebarSettings": "Einstellungen", - "sidebarAllUsers": "Alle Benutzer", - "sidebarIdentityProviders": "Identitätsanbieter", - "sidebarLicense": "Lizenz", - "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", - "viewDockerContainers": "Docker Container anzeigen", - "containersIn": "Container in {siteName}", - "selectContainerDescription": "Wählen Sie einen Container, der als Hostname für dieses Ziel verwendet werden soll. Klicken Sie auf einen Port, um einen Port zu verwenden.", - "containerName": "Name", - "containerImage": "Bild", - "containerState": "Bundesland", - "containerNetworks": "Netzwerke", - "containerHostnameIp": "Hostname/IP", - "containerLabels": "Etiketten", - "containerLabelsCount": "{count, plural, one {# Etikett} other {# Etiketten}}", - "containerLabelsTitle": "Container-Labels", - "containerLabelEmpty": "", - "containerPorts": "Häfen", - "containerPortsMore": "+{count} mehr", - "containerActions": "Aktionen", - "select": "Auswählen", - "noContainersMatchingFilters": "Es wurden keine Container gefunden, die den aktuellen Filtern entsprechen.", - "showContainersWithoutPorts": "Container ohne Ports anzeigen", - "showStoppedContainers": "Stoppte Container anzeigen", - "noContainersFound": "Keine Container gefunden. Stellen Sie sicher, dass Docker Container laufen.", - "searchContainersPlaceholder": "Durchsuche {count} Container...", - "searchResultsCount": "{count, plural, one {# Ergebnis} other {# Ergebnisse}}", - "filters": "Filter", - "filterOptions": "Filteroptionen", - "filterPorts": "Häfen", - "filterStopped": "Stoppt", - "clearAllFilters": "Alle Filter löschen", - "columns": "Spalten", - "toggleColumns": "Spalten umschalten", - "refreshContainersList": "Container-Liste aktualisieren", - "searching": "Suche...", - "noContainersFoundMatching": "Keine Container gefunden mit \"{filter}\".", - "light": "hell", - "dark": "dunkel", - "system": "System", - "theme": "Design", - "subnetRequired": "Subnetz ist erforderlich", - "initialSetupTitle": "Initial Einrichtung des Servers", - "initialSetupDescription": "Erstellen Sie das initiale Server-Admin-Konto. Es kann nur einen Server-Admin geben. Sie können diese Anmeldedaten später immer ändern.", - "createAdminAccount": "Admin-Konto erstellen", - "setupErrorCreateAdmin": "Beim Erstellen des Server-Admin-Kontos ist ein Fehler aufgetreten.", - "certificateStatus": "Zertifikatsstatus", - "loading": "Laden", - "restart": "Neustart", - "domains": "Domänen", - "domainsDescription": "Domains für Ihre Organisation verwalten", - "domainsSearch": "Domains durchsuchen...", - "domainAdd": "Domain hinzufügen", - "domainAddDescription": "Eine neue Domain in Ihrer Organisation registrieren", - "domainCreate": "Domain erstellen", - "domainCreatedDescription": "Domain erfolgreich erstellt", - "domainDeletedDescription": "Domain erfolgreich gelöscht", - "domainQuestionRemove": "Möchten Sie die Domain {domain} wirklich aus Ihrem Konto entfernen?", - "domainMessageRemove": "Nach dem Entfernen wird die Domain nicht mehr mit Ihrem Konto verknüpft.", - "domainMessageConfirm": "Um zu bestätigen, geben Sie bitte den Domainnamen unten ein.", - "domainConfirmDelete": "Domain-Löschung bestätigen", - "domainDelete": "Domain löschen", - "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)", - "selectDomainTypeCnameDescription": "Nur diese spezifische Domain. Verwenden Sie dies für einzelne Subdomains oder spezifische Domaineinträge.", - "selectDomainTypeWildcardName": "Wildcard-Domain", - "selectDomainTypeWildcardDescription": "Diese Domain und ihre Subdomains.", - "domainDelegation": "Einzelne Domain", - "selectType": "Typ auswählen", - "actions": "Aktionen", - "refresh": "Aktualisieren", - "refreshError": "Datenaktualisierung fehlgeschlagen", - "verified": "Verifiziert", - "pending": "Ausstehend", - "sidebarBilling": "Abrechnung", - "billing": "Abrechnung", - "orgBillingDescription": "Verwalten Sie Ihre Rechnungsinformationen und Abonnements", - "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", - "fossorial": "Fossorial", - "completeAccountSetup": "Kontoeinrichtung abschließen", - "completeAccountSetupDescription": "Legen Sie Ihr Passwort fest, um zu beginnen", - "accountSetupSent": "Wir senden einen Code für die Kontoeinrichtung an diese E-Mail-Adresse.", - "accountSetupCode": "Einrichtungscode", - "accountSetupCodeDescription": "Prüfen Sie Ihre E-Mail auf den Einrichtungscode.", - "passwordCreate": "Passwort erstellen", - "passwordCreateConfirm": "Passwort bestätigen", - "accountSetupSubmit": "Einrichtungscode senden", - "completeSetup": "Einrichtung abschließen", - "accountSetupSuccess": "Kontoeinrichtung abgeschlossen! Willkommen bei Pangolin!", - "documentation": "Dokumentation", - "saveAllSettings": "Alle Einstellungen speichern", - "settingsUpdated": "Einstellungen aktualisiert", - "settingsUpdatedDescription": "Alle Einstellungen wurden erfolgreich aktualisiert", - "settingsErrorUpdate": "Einstellungen konnten nicht aktualisiert werden", - "settingsErrorUpdateDescription": "Beim Aktualisieren der Einstellungen ist ein Fehler aufgetreten", - "sidebarCollapse": "Zusammenklappen", - "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": "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", - "domainPickerTabAll": "Alle", - "domainPickerTabOrganization": "Organisation", - "domainPickerTabProvided": "Bereitgestellt", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Verfügbarkeit prüfen...", - "domainPickerNoMatchingDomains": "Keine passenden Domains gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.", - "domainPickerOrganizationDomains": "Organisations-Domains", - "domainPickerProvidedDomains": "Bereitgestellte Domains", - "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", - "createNewOrgDescription": "Eine neue Organisation erstellen", - "organization": "Organisation", - "port": "Port", - "securityKeyManage": "Sicherheitsschlüssel verwalten", - "securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen", - "securityKeyRegister": "Neuen Sicherheitsschlüssel registrieren", - "securityKeyList": "Ihre Sicherheitsschlüssel", - "securityKeyNone": "Noch keine Sicherheitsschlüssel registriert", - "securityKeyNameRequired": "Name ist erforderlich", - "securityKeyRemove": "Entfernen", - "securityKeyLastUsed": "Zuletzt verwendet: {date}", - "securityKeyNameLabel": "Name", - "securityKeyRegisterSuccess": "Sicherheitsschlüssel erfolgreich registriert", - "securityKeyRegisterError": "Fehler beim Registrieren des Sicherheitsschlüssels", - "securityKeyRemoveSuccess": "Sicherheitsschlüssel erfolgreich entfernt", - "securityKeyRemoveError": "Fehler beim Entfernen des Sicherheitsschlüssels", - "securityKeyLoadError": "Fehler beim Laden der Sicherheitsschlüssel", - "securityKeyLogin": "Mit dem Sicherheitsschlüssel fortfahren", - "securityKeyAuthError": "Fehler bei der Authentifizierung mit Sicherheitsschlüssel", - "securityKeyRecommendation": "Erwägen Sie die Registrierung eines weiteren Sicherheitsschlüssels auf einem anderen Gerät, um sicherzustellen, dass Sie sich nicht aus Ihrem Konto aussperren.", - "registering": "Registrierung...", - "securityKeyPrompt": "Bitte bestätigen Sie Ihre Identität mit Ihrem Sicherheitsschlüssel. Stellen Sie sicher, dass Ihr Sicherheitsschlüssel verbunden und einsatzbereit ist.", - "securityKeyBrowserNotSupported": "Ihr Browser unterstützt Sicherheitsschlüssel nicht. Bitte verwenden Sie einen modernen Browser wie Chrome, Firefox oder Safari.", - "securityKeyPermissionDenied": "Bitte erlauben Sie den Zugriff auf Ihren Sicherheitsschlüssel, um sich weiter anzumelden.", - "securityKeyRemovedTooQuickly": "Lassen Sie Ihren Sicherheitsschlüssel verbunden, bis der Anmeldeprozess abgeschlossen ist.", - "securityKeyNotSupported": "Ihr Sicherheitsschlüssel ist möglicherweise nicht kompatibel. Bitte versuchen Sie einen anderen Sicherheitsschlüssel.", - "securityKeyUnknownError": "Es gab ein Problem mit Ihrem Sicherheitsschlüssel. Bitte versuchen Sie es erneut.", - "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.", - "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", - "securityKeyTwoFactorRequired": "Zwei-Faktor-Authentifizierung erforderlich", - "securityKeyTwoFactorDescription": "Bitte geben Sie Ihren Zwei-Faktor-Authentifizierungscode ein, um den Sicherheitsschlüssel zu registrieren", - "securityKeyTwoFactorRemoveDescription": "Bitte geben Sie Ihren Zwei-Faktor-Authentifizierungscode ein, um den Sicherheitsschlüssel zu entfernen", - "securityKeyTwoFactorCode": "Zwei-Faktor-Code", - "securityKeyRemoveTitle": "Sicherheitsschlüssel entfernen", - "securityKeyRemoveDescription": "Geben Sie Ihr Passwort ein, um den Sicherheitsschlüssel \"{name}\" zu entfernen", - "securityKeyNoKeysRegistered": "Keine Sicherheitsschlüssel registriert", - "securityKeyNoKeysDescription": "Fügen Sie einen Sicherheitsschlüssel hinzu, um die Sicherheit Ihres Kontos zu erhöhen", - "createDomainRequired": "Domain ist erforderlich", - "createDomainAddDnsRecords": "DNS-Einträge hinzufügen", - "createDomainAddDnsRecordsDescription": "Fügen Sie die folgenden DNS-Einträge zu Ihrem Domain-Provider hinzu, um die Einrichtung abzuschließen.", - "createDomainNsRecords": "NS-Einträge", - "createDomainRecord": "Eintrag", - "createDomainType": "Typ:", - "createDomainName": "Name:", - "createDomainValue": "Wert:", - "createDomainCnameRecords": "CNAME-Einträge", - "createDomainARecords": "A-Aufzeichnungen", - "createDomainRecordNumber": "Eintrag {number}", - "createDomainTxtRecords": "TXT-Einträge", - "createDomainSaveTheseRecords": "Diese Einträge speichern", - "createDomainSaveTheseRecordsDescription": "Achten Sie darauf, diese DNS-Einträge zu speichern, da Sie sie nicht erneut sehen werden.", - "createDomainDnsPropagation": "DNS-Verbreitung", - "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", - "and": "und", - "privacyPolicy": "Datenschutzrichtlinie" - }, - "siteRequired": "Standort ist erforderlich.", - "olmTunnel": "Olm-Tunnel", - "olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung", - "errorCreatingClient": "Fehler beim Erstellen des Clients", - "clientDefaultsNotFound": "Kundenvorgaben nicht gefunden", - "createClient": "Client erstellen", - "createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.", - "seeAllClients": "Alle Clients anzeigen", - "clientInformation": "Kundeninformationen", - "clientNamePlaceholder": "Kundenname", - "address": "Adresse", - "subnetPlaceholder": "Subnetz", - "addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.", - "selectSites": "Standorte auswählen", - "sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.", - "clientInstallOlm": "Olm installieren", - "clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen", - "clientOlmCredentials": "Olm-Zugangsdaten", - "clientOlmCredentialsDescription": "So authentifiziert sich Olm beim Server", - "olmEndpoint": "Olm-Endpunkt", - "olmId": "Olm-ID", - "olmSecretKey": "Olm-Geheimschlüssel", - "clientCredentialsSave": "Speichern Sie Ihre Zugangsdaten", - "clientCredentialsSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", - "generalSettingsDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diesen Client", - "clientUpdated": "Client aktualisiert", - "clientUpdatedDescription": "Der Client wurde aktualisiert.", - "clientUpdateFailed": "Fehler beim Aktualisieren des Clients", - "clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.", - "sitesFetchFailed": "Fehler beim Abrufen von Standorten", - "sitesFetchError": "Beim Abrufen von Standorten ist ein Fehler aufgetreten.", - "olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.", - "olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.", - "remoteSubnets": "Remote-Subnetze", - "enterCidrRange": "Geben Sie den CIDR-Bereich ein", - "remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die über Clients von dieser Site aus remote zugänglich sind. Verwenden Sie ein Format wie 10.0.0.0/24. Dies gilt NUR für die VPN-Client-Konnektivität.", - "resourceEnableProxy": "Öffentlichen Proxy aktivieren", - "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", - "externalProxyEnabled": "Externer Proxy aktiviert", - "addNewTarget": "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", - "domainPickerBaseDomainLabel": "Basisdomäne", - "domainPickerSearchDomains": "Domains suchen...", - "domainPickerNoDomainsFound": "Keine Domains gefunden", - "domainPickerLoadingDomains": "Domains werden geladen...", - "domainPickerSelectBaseDomain": "Basisdomäne auswählen...", - "domainPickerNotAvailableForCname": "Für CNAME-Domains nicht verfügbar", - "domainPickerEnterSubdomainOrLeaveBlank": "Geben Sie eine Subdomain ein oder lassen Sie das Feld leer, um die Basisdomäne zu verwenden.", - "domainPickerEnterSubdomainToSearch": "Geben Sie eine Subdomain ein, um verfügbare freie Domains zu suchen und auszuwählen.", - "domainPickerFreeDomains": "Freie Domains", - "domainPickerSearchForAvailableDomains": "Verfügbare Domains suchen", - "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", - "resourcesTableProxyResources": "Proxy-Ressourcen", - "resourcesTableClientResources": "Client-Ressourcen", - "resourcesTableNoProxyResourcesFound": "Keine Proxy-Ressourcen gefunden.", - "resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.", - "resourcesTableDestination": "Ziel", - "resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit", - "resourcesTableClients": "Kunden", - "resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.", - "editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten", - "editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.", - "editInternalResourceDialogResourceProperties": "Ressourceneigenschaften", - "editInternalResourceDialogName": "Name", - "editInternalResourceDialogProtocol": "Protokoll", - "editInternalResourceDialogSitePort": "Site-Port", - "editInternalResourceDialogTargetConfiguration": "Zielkonfiguration", - "editInternalResourceDialogCancel": "Abbrechen", - "editInternalResourceDialogSaveResource": "Ressource speichern", - "editInternalResourceDialogSuccess": "Erfolg", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne Ressource erfolgreich aktualisiert", - "editInternalResourceDialogError": "Fehler", - "editInternalResourceDialogFailedToUpdateInternalResource": "Interne Ressource konnte nicht aktualisiert werden", - "editInternalResourceDialogNameRequired": "Name ist erforderlich", - "editInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein", - "editInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein", - "editInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein", - "editInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", - "editInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", - "editInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", - "createInternalResourceDialogNoSitesAvailable": "Keine Sites verfügbar", - "createInternalResourceDialogNoSitesAvailableDescription": "Sie müssen mindestens eine Newt-Site mit einem konfigurierten Subnetz haben, um interne Ressourcen zu erstellen.", - "createInternalResourceDialogClose": "Schließen", - "createInternalResourceDialogCreateClientResource": "Ressource erstellen", - "createInternalResourceDialogCreateClientResourceDescription": "Erstellen Sie eine neue Ressource, die für Clients zugänglich ist, die mit der ausgewählten Site verbunden sind.", - "createInternalResourceDialogResourceProperties": "Ressourceneigenschaften", - "createInternalResourceDialogName": "Name", - "createInternalResourceDialogSite": "Standort", - "createInternalResourceDialogSelectSite": "Standort auswählen...", - "createInternalResourceDialogSearchSites": "Sites durchsuchen...", - "createInternalResourceDialogNoSitesFound": "Keine Standorte gefunden.", - "createInternalResourceDialogProtocol": "Protokoll", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site-Port", - "createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.", - "createInternalResourceDialogTargetConfiguration": "Zielkonfiguration", - "createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse oder Hostname Adresse der Ressource im Netzwerk der Website.", - "createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.", - "createInternalResourceDialogCancel": "Abbrechen", - "createInternalResourceDialogCreateResource": "Ressource erstellen", - "createInternalResourceDialogSuccess": "Erfolg", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne Ressource erfolgreich erstellt", - "createInternalResourceDialogError": "Fehler", - "createInternalResourceDialogFailedToCreateInternalResource": "Interne Ressource konnte nicht erstellt werden", - "createInternalResourceDialogNameRequired": "Name ist erforderlich", - "createInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein", - "createInternalResourceDialogPleaseSelectSite": "Bitte wählen Sie eine Site aus", - "createInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein", - "createInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein", - "createInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat", - "createInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein", - "createInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein", - "siteConfiguration": "Konfiguration", - "siteAcceptClientConnections": "Clientverbindungen akzeptieren", - "siteAcceptClientConnectionsDescription": "Erlauben Sie anderen Geräten, über diese Newt-Instanz mit Clients als Gateway zu verbinden.", - "siteAddress": "Site-Adresse", - "siteAddressDescription": "Geben Sie die IP-Adresse des Hosts an, mit dem sich die Clients verbinden sollen. Dies ist die interne Adresse der Site im Pangolin-Netzwerk, die von Clients angesprochen werden muss. Muss innerhalb des Unternehmens-Subnetzes liegen.", - "autoLoginExternalIdp": "Automatische Anmeldung mit externem IDP", - "autoLoginExternalIdpDescription": "Leiten Sie den Benutzer sofort zur Authentifizierung an den externen IDP weiter.", - "selectIdp": "IDP auswählen", - "selectIdpPlaceholder": "Wählen Sie einen IDP...", - "selectIdpRequired": "Bitte wählen Sie einen IDP aus, wenn automatische Anmeldung aktiviert ist.", - "autoLoginTitle": "Weiterleitung", - "autoLoginDescription": "Sie werden zum externen Identitätsanbieter zur Authentifizierung weitergeleitet.", - "autoLoginProcessing": "Authentifizierung vorbereiten...", - "autoLoginRedirecting": "Weiterleitung zur Anmeldung...", - "autoLoginError": "Fehler bei der automatischen Anmeldung", - "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", - "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.", - "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", - "introTitle": "Verwalteter selbstgehosteter Pangolin", - "introDescription": "ist eine Deployment-Option, die für Personen konzipiert wurde, die Einfachheit und zusätzliche Zuverlässigkeit wünschen, während sie ihre Daten privat und selbstgehostet halten.", - "introDetail": "Mit dieser Option haben Sie immer noch Ihren eigenen Pangolin-Knoten – Ihre Tunnel, SSL-Terminierung und Traffic bleiben auf Ihrem Server. Der Unterschied besteht darin, dass Verwaltung und Überwachung über unser Cloud-Dashboard abgewickelt werden, das eine Reihe von Vorteilen freischaltet:", - "benefitSimplerOperations": { - "title": "Einfachere Operationen", - "description": "Sie brauchen keinen eigenen Mail-Server auszuführen oder komplexe Warnungen einzurichten. Sie erhalten Gesundheitschecks und Ausfallwarnungen aus dem Box." - }, - "benefitAutomaticUpdates": { - "title": "Automatische Updates", - "description": "Das Cloud-Dashboard entwickelt sich schnell, so dass Sie neue Funktionen und Fehlerbehebungen erhalten, ohne jedes Mal neue Container manuell ziehen zu müssen." - }, - "benefitLessMaintenance": { - "title": "Weniger Wartung", - "description": "Keine Datenbankmigrationen, Sicherungen oder zusätzliche Infrastruktur zum Verwalten. Wir kümmern uns um das in der Cloud." - }, - "benefitCloudFailover": { - "title": "Cloud-Ausfall", - "description": "Wenn Ihr Knoten runtergeht, können Ihre Tunnel vorübergehend an unsere Cloud-Punkte scheitern, bis Sie ihn wieder online bringen." - }, - "benefitHighAvailability": { - "title": "Hohe Verfügbarkeit (PoPs)", - "description": "Sie können auch mehrere Knoten an Ihr Konto anhängen, um Redundanz und bessere Leistung zu erzielen." - }, - "benefitFutureEnhancements": { - "title": "Zukünftige Verbesserungen", - "description": "Wir planen weitere Analyse-, Alarm- und Management-Tools hinzuzufügen, um Ihren Einsatz noch robuster zu machen." - }, - "docsAlert": { - "text": "Erfahren Sie mehr über die Managed Self-Hosted Option in unserer", - "documentation": "dokumentation" - }, - "convertButton": "Diesen Knoten in Managed Self-Hosted umwandeln" - }, - "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", - "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", - "domainPickerUnverified": "Nicht verifiziert", - "domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.", - "domainPickerError": "Fehler", - "domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domänen", - "domainPickerErrorCheckAvailability": "Fehler beim Prüfen der Domain-Verfügbarkeit", - "domainPickerInvalidSubdomain": "Ungültige Subdomain", - "domainPickerInvalidSubdomainRemoved": "Die Eingabe \"{sub}\" wurde entfernt, weil sie nicht gültig ist.", - "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", - "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 deleted file mode 100644 index a9f78517..00000000 --- a/messages/en-US.json +++ /dev/null @@ -1,1898 +0,0 @@ -{ - "setupCreate": "Create your organization, site, and resources", - "setupNewOrg": "New Organization", - "setupCreateOrg": "Create Organization", - "setupCreateResources": "Create Resources", - "setupOrgName": "Organization Name", - "orgDisplayName": "This is the display name of your organization.", - "orgId": "Organization ID", - "setupIdentifierMessage": "This is the unique identifier for your organization. This is separate from the display name.", - "setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.", - "componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.", - "componentsErrorNoMember": "You are not currently a member of any organizations.", - "welcome": "Welcome!", - "welcomeTo": "Welcome to", - "componentsCreateOrg": "Create an Organization", - "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", - "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", - "dismiss": "Dismiss", - "componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.", - "componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!", - "inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.", - "inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.", - "inviteLoginUser": "Please make sure you're logged in as the correct user.", - "inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.", - "inviteCreateUser": "Please create an account first.", - "goHome": "Go Home", - "inviteLogInOtherUser": "Log In as a Different User", - "createAnAccount": "Create an Account", - "inviteNotAccepted": "Invite Not Accepted", - "authCreateAccount": "Create an account to get started", - "authNoAccount": "Don't have an account?", - "email": "Email", - "password": "Password", - "confirmPassword": "Confirm Password", - "createAccount": "Create Account", - "viewSettings": "View settings", - "delete": "Delete", - "name": "Name", - "online": "Online", - "offline": "Offline", - "site": "Site", - "dataIn": "Data In", - "dataOut": "Data Out", - "connectionType": "Connection Type", - "tunnelType": "Tunnel Type", - "local": "Local", - "edit": "Edit", - "siteConfirmDelete": "Confirm Delete Site", - "siteDelete": "Delete Site", - "siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.", - "siteMessageConfirm": "To confirm, please type the name of the site below.", - "siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?", - "siteManageSites": "Manage Sites", - "siteDescription": "Allow connectivity to your network through secure tunnels", - "siteCreate": "Create Site", - "siteCreateDescription2": "Follow the steps below to create and connect a new site", - "siteCreateDescription": "Create a new site to start connecting your resources", - "close": "Close", - "siteErrorCreate": "Error creating site", - "siteErrorCreateKeyPair": "Key pair or site defaults not found", - "siteErrorCreateDefaults": "Site defaults not found", - "method": "Method", - "siteMethodDescription": "This is how you will expose connections.", - "siteLearnNewt": "Learn how to install Newt on your system", - "siteSeeConfigOnce": "You will only be able to see the configuration once.", - "siteLoadWGConfig": "Loading WireGuard configuration...", - "siteDocker": "Expand for Docker Deployment Details", - "toggle": "Toggle", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "Local sites do not tunnel, learn more", - "siteConfirmCopy": "I have copied the config", - "searchSitesProgress": "Search sites...", - "siteAdd": "Add Site", - "siteInstallNewt": "Install Newt", - "siteInstallNewtDescription": "Get Newt running on your system", - "WgConfiguration": "WireGuard Configuration", - "WgConfigurationDescription": "Use the following configuration to connect to your network", - "operatingSystem": "Operating System", - "commands": "Commands", - "recommended": "Recommended", - "siteNewtDescription": "For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard.", - "siteRunsInDocker": "Runs in Docker", - "siteRunsInShell": "Runs in shell on macOS, Linux, and Windows", - "siteErrorDelete": "Error deleting site", - "siteErrorUpdate": "Failed to update site", - "siteErrorUpdateDescription": "An error occurred while updating the site.", - "siteUpdated": "Site updated", - "siteUpdatedDescription": "The site has been updated.", - "siteGeneralDescription": "Configure the general settings for this site", - "siteSettingDescription": "Configure the settings on your site", - "siteSetting": "{siteName} Settings", - "siteNewtTunnel": "Newt Tunnel (Recommended)", - "siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.", - "siteWg": "Basic WireGuard", - "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", - "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 available on remote 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. 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", - "resourceInfo": "Resource Information", - "resourceNameDescription": "This is the display name for the resource.", - "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", - "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", - "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", - "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", - "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": "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", - "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", - "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 {count, plural, =1 {role} other {roles}}:", - "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", - "licenseTierPersonal": "Personal License", - "licensed": "Licensed", - "yes": "Yes", - "no": "No", - "sitesAdditional": "Additional Sites", - "licenseKeys": "License Keys", - "sitestCountDecrease": "Decrease site count", - "sitestCountIncrease": "Increase site count", - "idpManage": "Manage Identity Providers", - "idpManageDescription": "View and manage identity providers in the system", - "idpDeletedDescription": "Identity provider deleted successfully", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?", - "idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.", - "idpMessageConfirm": "To confirm, please type the name of the identity provider below.", - "idpConfirmDelete": "Confirm Delete Identity Provider", - "idpDelete": "Delete Identity Provider", - "idp": "Identity Providers", - "idpSearch": "Search identity providers...", - "idpAdd": "Add Identity Provider", - "idpClientIdRequired": "Client ID is required.", - "idpClientSecretRequired": "Client Secret is required.", - "idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.", - "idpErrorTokenUrlInvalid": "Token URL must be a valid URL.", - "idpPathRequired": "Identifier Path is required.", - "idpScopeRequired": "Scopes are required.", - "idpOidcDescription": "Configure an OpenID Connect identity provider", - "idpCreatedDescription": "Identity provider created successfully", - "idpCreate": "Create Identity Provider", - "idpCreateDescription": "Configure a new identity provider for user authentication", - "idpSeeAll": "See All Identity Providers", - "idpSettingsDescription": "Configure the basic information for your identity provider", - "idpDisplayName": "A display name for this identity provider", - "idpAutoProvisionUsers": "Auto Provision Users", - "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", - "licenseBadge": "EE", - "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", - "inviteInvalid": "Invalid Invite", - "inviteInvalidDescription": "The invite link is invalid.", - "inviteErrorWrongUser": "Invite is not for this user", - "inviteErrorUserNotExists": "User does not exist. Please create an account first.", - "inviteErrorLoginRequired": "You must be logged in to accept an invite", - "inviteErrorExpired": "The invite may have expired", - "inviteErrorRevoked": "The invite might have been revoked", - "inviteErrorTypo": "There could be a typo in the invite link", - "pangolinSetup": "Setup - Pangolin", - "orgNameRequired": "Organization name is required", - "orgIdRequired": "Organization ID is required", - "orgErrorCreate": "An error occurred while creating org", - "pageNotFound": "Page Not Found", - "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", - "overview": "Overview", - "home": "Home", - "accessControl": "Access Control", - "settings": "Settings", - "usersAll": "All Users", - "license": "License", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "No results found.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Entered Tags", - "tagsEnteredDescription": "These are the tags you`ve entered.", - "tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options", - "tagsWarnInvalid": "Invalid tag as per validateTag", - "tagWarnTooShort": "Tag {tagText} is too short", - "tagWarnTooLong": "Tag {tagText} is too long", - "tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed", - "tagWarnDuplicate": "Duplicate tag {tagText} not added", - "supportKeyInvalid": "Invalid Key", - "supportKeyInvalidDescription": "Your supporter key is invalid.", - "supportKeyValid": "Valid Key", - "supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!", - "supportKeyErrorValidationDescription": "Failed to validate supporter key.", - "supportKey": "Support Development and Adopt a Pangolin!", - "supportKeyDescription": "Purchase a supporter key to help us continue developing Pangolin for the community. Your contribution allows us to commit more time to maintain and add new features to the application for everyone. We will never use this to paywall features. This is separate from any Commercial Edition.", - "supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!", - "supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on", - "supportKeyPurchaseLink": "our website", - "supportKeyPurchase2": "and redeem it here.", - "supportKeyLearnMore": "Learn more.", - "supportKeyOptions": "Please select the option that best suits you.", - "supportKetOptionFull": "Full Supporter", - "forWholeServer": "For the whole server", - "lifetimePurchase": "Lifetime purchase", - "supporterStatus": "Supporter status", - "buy": "Buy", - "supportKeyOptionLimited": "Limited Supporter", - "forFiveUsers": "For 5 or less users", - "supportKeyRedeem": "Redeem Supporter Key", - "supportKeyHideSevenDays": "Hide for 7 days", - "supportKeyEnter": "Enter Supporter Key", - "supportKeyEnterDescription": "Meet your very own pet Pangolin!", - "githubUsername": "GitHub Username", - "supportKeyInput": "Supporter Key", - "supportKeyBuy": "Buy Supporter Key", - "logoutError": "Error logging out", - "signingAs": "Signed in as", - "serverAdmin": "Server Admin", - "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", - "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", - "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", - "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", - "fossorial": "Fossorial", - "completeAccountSetup": "Complete Account Setup", - "completeAccountSetupDescription": "Set your password to get started", - "accountSetupSent": "We'll send an account setup code to this email address.", - "accountSetupCode": "Setup Code", - "accountSetupCodeDescription": "Check your email for the setup code.", - "passwordCreate": "Create Password", - "passwordCreateConfirm": "Confirm Password", - "accountSetupSubmit": "Send Setup Code", - "completeSetup": "Complete Setup", - "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", - "documentation": "Documentation", - "saveAllSettings": "Save All Settings", - "settingsUpdated": "Settings updated", - "settingsUpdatedDescription": "All settings have been updated successfully", - "settingsErrorUpdate": "Failed to update settings", - "settingsErrorUpdateDescription": "An error occurred while updating settings", - "sidebarCollapse": "Collapse", - "sidebarExpand": "Expand", - "newtUpdateAvailable": "Update Available", - "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", - "domainPickerEnterDomain": "Domain", - "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "Enter the full domain of the resource to see available options.", - "domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options", - "domainPickerTabAll": "All", - "domainPickerTabOrganization": "Organization", - "domainPickerTabProvided": "Provided", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Checking availability...", - "domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.", - "domainPickerOrganizationDomains": "Organization Domains", - "domainPickerProvidedDomains": "Provided Domains", - "domainPickerSubdomain": "Subdomain: {subdomain}", - "domainPickerNamespace": "Namespace: {namespace}", - "domainPickerShowMore": "Show More", - "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", - "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.", - "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", - "billingPricingCalculatorLink": "Pricing Calculator", - "signUpTerms": { - "IAgreeToThe": "I agree to the", - "termsOfService": "terms of service", - "and": "and", - "privacyPolicy": "privacy policy" - }, - "siteRequired": "Site is required.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm for client connectivity", - "errorCreatingClient": "Error creating client", - "clientDefaultsNotFound": "Client defaults not found", - "createClient": "Create Client", - "createClientDescription": "Create a new client for connecting to your sites", - "seeAllClients": "See All Clients", - "clientInformation": "Client Information", - "clientNamePlaceholder": "Client name", - "address": "Address", - "subnetPlaceholder": "Subnet", - "addressDescription": "The address that this client will use for connectivity", - "selectSites": "Select sites", - "sitesDescription": "The client will have connectivity to the selected sites", - "clientInstallOlm": "Install Olm", - "clientInstallOlmDescription": "Get Olm running on your system", - "clientOlmCredentials": "Olm Credentials", - "clientOlmCredentialsDescription": "This is how Olm will authenticate with the server", - "olmEndpoint": "Olm Endpoint", - "olmId": "Olm ID", - "olmSecretKey": "Olm Secret Key", - "clientCredentialsSave": "Save Your Credentials", - "clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", - "generalSettingsDescription": "Configure the general settings for this client", - "clientUpdated": "Client updated", - "clientUpdatedDescription": "The client has been updated.", - "clientUpdateFailed": "Failed to update client", - "clientUpdateError": "An error occurred while updating the client.", - "sitesFetchFailed": "Failed to fetch sites", - "sitesFetchError": "An error occurred while fetching sites.", - "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", - "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", - "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can 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", - "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", - "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", - "domainPickerNotWorkSelfHosted": "Note: Free provided domains are not available for self-hosted instances right now.", - "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", - "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.", - "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", - "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:", - "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." - }, - "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." - }, - "benefitLessMaintenance": { - "title": "Less maintenance", - "description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud." - }, - "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." - }, - "benefitHighAvailability": { - "title": "High availability (PoPs)", - "description": "You can also attach multiple nodes to your account for redundancy and better performance." - }, - "benefitFutureEnhancements": { - "title": "Future enhancements", - "description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust." - }, - "docsAlert": { - "text": "Learn more about the Managed Self-Hosted option in our", - "documentation": "documentation" - }, - "convertButton": "Convert This Node to Managed Self-Hosted" - }, - "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", - "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", - "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}\"", - "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", - "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 deleted file mode 100644 index 17f9ad44..00000000 --- a/messages/es-ES.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Crea tu organización, sitio y recursos", - "setupNewOrg": "Nueva organización", - "setupCreateOrg": "Crear organización", - "setupCreateResources": "Crear Recursos", - "setupOrgName": "Nombre de la organización", - "orgDisplayName": "Este es el nombre mostrado de su organización.", - "orgId": "ID de la organización", - "setupIdentifierMessage": "Este es el identificador único para su organización. Esto es independiente del nombre de la pantalla.", - "setupErrorIdentifier": "El ID de la organización ya está en uso. Por favor, elija uno diferente.", - "componentsErrorNoMemberCreate": "Actualmente no eres miembro de ninguna organización. Crea una organización para empezar.", - "componentsErrorNoMember": "Actualmente no eres miembro de ninguna organización.", - "welcome": "Bienvenido a Pangolin", - "welcomeTo": "Bienvenido a", - "componentsCreateOrg": "Crear una organización", - "componentsMember": "Eres un miembro de {count, plural, =0 {ninguna organización} one {una organización} other {# organizaciones}}.", - "componentsInvalidKey": "Se han detectado claves de licencia inválidas o caducadas. Siga los términos de licencia para seguir usando todas las características.", - "dismiss": "Descartar", - "componentsLicenseViolation": "Violación de la Licencia: Este servidor está usando sitios {usedSites} que exceden su límite de licencias de sitios {maxSites} . Siga los términos de licencia para seguir usando todas las características.", - "componentsSupporterMessage": "¡Gracias por apoyar a Pangolin como {tier}!", - "inviteErrorNotValid": "Lo sentimos, pero parece que la invitación a la que intentas acceder no ha sido aceptada o ya no es válida.", - "inviteErrorUser": "Lo sentimos, pero parece que la invitación a la que intentas acceder no es para este usuario.", - "inviteLoginUser": "Por favor, asegúrese de que ha iniciado sesión como el usuario correcto.", - "inviteErrorNoUser": "Lo sentimos, pero parece que la invitación a la que intentas acceder no es para un usuario que existe.", - "inviteCreateUser": "Por favor, cree una cuenta primero.", - "goHome": "Ir a casa", - "inviteLogInOtherUser": "Iniciar sesión como un usuario diferente", - "createAnAccount": "Crear una cuenta", - "inviteNotAccepted": "Invitación no aceptada", - "authCreateAccount": "Crear una cuenta para empezar", - "authNoAccount": "¿No tienes una cuenta?", - "email": "E-mail", - "password": "Contraseña", - "confirmPassword": "Confirmar contraseña", - "createAccount": "Crear cuenta", - "viewSettings": "Ver ajustes", - "delete": "Eliminar", - "name": "Nombre", - "online": "En línea", - "offline": "Desconectado", - "site": "Sitio", - "dataIn": "Datos en", - "dataOut": "Datos Fuentes", - "connectionType": "Tipo de conexión", - "tunnelType": "Tipo de túnel", - "local": "Local", - "edit": "Editar", - "siteConfirmDelete": "Confirmar Borrar Sitio", - "siteDelete": "Eliminar sitio", - "siteMessageRemove": "Una vez eliminado, el sitio ya no será accesible. Todos los recursos y objetivos asociados con el sitio también serán eliminados.", - "siteMessageConfirm": "Para confirmar, por favor escriba el nombre del sitio a continuación.", - "siteQuestionRemove": "¿Está seguro de que desea eliminar el sitio {selectedSite} de la organización?", - "siteManageSites": "Administrar Sitios", - "siteDescription": "Permitir conectividad a tu red a través de túneles seguros", - "siteCreate": "Crear sitio", - "siteCreateDescription2": "Siga los pasos siguientes para crear y conectar un nuevo sitio", - "siteCreateDescription": "Crear un nuevo sitio para comenzar a conectar sus recursos", - "close": "Cerrar", - "siteErrorCreate": "Error al crear el sitio", - "siteErrorCreateKeyPair": "Por defecto no se encuentra el par de claves o el sitio", - "siteErrorCreateDefaults": "Sitio por defecto no encontrado", - "method": "Método", - "siteMethodDescription": "Así es como se expondrán las conexiones.", - "siteLearnNewt": "Aprende cómo instalar Newt en tu sistema", - "siteSeeConfigOnce": "Sólo podrá ver la configuración una vez.", - "siteLoadWGConfig": "Cargando configuración de WireGuard...", - "siteDocker": "Expandir para detalles de despliegue de Docker", - "toggle": "Cambiar", - "dockerCompose": "Componer Docker", - "dockerRun": "Ejecutar Docker", - "siteLearnLocal": "Los sitios locales no tienen túnel, aprender más", - "siteConfirmCopy": "He copiado la configuración", - "searchSitesProgress": "Buscar sitios...", - "siteAdd": "Añadir sitio", - "siteInstallNewt": "Instalar Newt", - "siteInstallNewtDescription": "Recibe Newt corriendo en tu sistema", - "WgConfiguration": "Configuración de Wirex Guard", - "WgConfigurationDescription": "Utilice la siguiente configuración para conectarse a su red", - "operatingSystem": "Sistema operativo", - "commands": "Comandos", - "recommended": "Recomendado", - "siteNewtDescription": "Para la mejor experiencia de usuario, utilice Newt. Utiliza Wirex Guard bajo la capa y te permite dirigirte a tus recursos privados mediante su dirección LAN en tu red privada desde el panel de control de Pangolin.", - "siteRunsInDocker": "Ejecutar en Docker", - "siteRunsInShell": "Ejecuta en el shell en macOS, Linux y Windows", - "siteErrorDelete": "Error al eliminar el sitio", - "siteErrorUpdate": "Error al actualizar el sitio", - "siteErrorUpdateDescription": "Se ha producido un error al actualizar el sitio.", - "siteUpdated": "Sitio actualizado", - "siteUpdatedDescription": "El sitio ha sido actualizado.", - "siteGeneralDescription": "Configurar la configuración general de este sitio", - "siteSettingDescription": "Configurar la configuración de su sitio", - "siteSetting": "Ajustes {siteName}", - "siteNewtTunnel": "Túnel Nuevo (Recomendado)", - "siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.", - "siteWg": "Wirex Guardia Básica", - "siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.", - "siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", - "siteLocalDescription": "Solo recursos locales. Sin túneles.", - "siteLocalDescriptionSaas": "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", - "siteNewtCredentialsDescription": "Así es como Newt se autentificará con el servidor", - "siteCredentialsSave": "Guarda tus credenciales", - "siteCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", - "siteInfo": "Información del sitio", - "status": "Estado", - "shareTitle": "Administrar Enlaces de Compartir", - "shareDescription": "Crear enlaces compartidos para conceder acceso temporal o permanente a tus recursos", - "shareSearch": "Buscar enlaces compartidos...", - "shareCreate": "Crear enlace Compartir", - "shareErrorDelete": "Error al eliminar el enlace", - "shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace", - "shareDeleted": "Enlace eliminado", - "shareDeletedDescription": "El enlace ha sido eliminado", - "shareTokenDescription": "Su token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.", - "accessToken": "Token de acceso", - "usageExamples": "Ejemplos de uso", - "tokenId": "ID de token", - "requestHeades": "Solicitar cabeceras", - "queryParameter": "Parámetro de consulta", - "importantNote": "Nota Importante", - "shareImportantDescription": "Por razones de seguridad, el uso de cabeceras se recomienda sobre parámetros de consulta cuando sea posible, ya que los parámetros de consulta pueden ser registrados en los registros del servidor o en el historial del navegador.", - "token": "Token", - "shareTokenSecurety": "Mantenga su token de acceso seguro. No lo comparta en áreas de acceso público o código del lado del cliente.", - "shareErrorFetchResource": "No se pudo obtener recursos", - "shareErrorFetchResourceDescription": "Se ha producido un error al recuperar los recursos", - "shareErrorCreate": "Error al crear el enlace compartir", - "shareErrorCreateDescription": "Se ha producido un error al crear el enlace compartido", - "shareCreateDescription": "Cualquiera con este enlace puede acceder al recurso", - "shareTitleOptional": "Título (opcional)", - "expireIn": "Caduca en", - "neverExpire": "Nunca expirar", - "shareExpireDescription": "El tiempo de caducidad es cuánto tiempo el enlace será utilizable y proporcionará acceso al recurso. Después de este tiempo, el enlace ya no funcionará, y los usuarios que usaron este enlace perderán el acceso al recurso.", - "shareSeeOnce": "Sólo podrá ver este enlace una vez. Asegúrese de copiarlo.", - "shareAccessHint": "Cualquiera con este enlace puede acceder al recurso. Compártelo con cuidado.", - "shareTokenUsage": "Ver Uso de Token de Acceso", - "createLink": "Crear enlace", - "resourcesNotFound": "No se encontraron recursos", - "resourceSearch": "Buscar recursos", - "openMenu": "Abrir menú", - "resource": "Recurso", - "title": "Título", - "created": "Creado", - "expires": "Caduca", - "never": "Nunca", - "shareErrorSelectResource": "Por favor, seleccione un recurso", - "resourceTitle": "Administrar recursos", - "resourceDescription": "Crea proxies seguros para tus aplicaciones privadas", - "resourcesSearch": "Buscar recursos...", - "resourceAdd": "Añadir Recurso", - "resourceErrorDelte": "Error al eliminar el recurso", - "authentication": "Autenticación", - "protected": "Protegido", - "notProtected": "No protegido", - "resourceMessageRemove": "Una vez eliminado, el recurso ya no será accesible. Todos los objetivos asociados con el recurso también serán eliminados.", - "resourceMessageConfirm": "Para confirmar, por favor escriba el nombre del recurso a continuación.", - "resourceQuestionRemove": "¿Está seguro de que desea eliminar el recurso {selectedResource} de la organización?", - "resourceHTTP": "HTTPS Recurso", - "resourceHTTPDescription": "Solicitudes de proxy a tu aplicación sobre HTTPS usando un subdominio o dominio base.", - "resourceRaw": "Recurso TCP/UDP sin procesar", - "resourceRawDescription": "Solicitudes de proxy a tu aplicación a través de TCP/UDP usando un número de puerto.", - "resourceCreate": "Crear Recurso", - "resourceCreateDescription": "Siga los siguientes pasos para crear un nuevo recurso", - "resourceSeeAll": "Ver todos los recursos", - "resourceInfo": "Información del recurso", - "resourceNameDescription": "Este es el nombre para mostrar el recurso.", - "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", - "resourceHTTPSSettings": "Configuración HTTPS", - "resourceHTTPSSettingsDescription": "Configurar cómo se accederá a tu recurso a través de HTTPS", - "domainType": "Tipo de dominio", - "subdomain": "Subdominio", - "baseDomain": "Dominio base", - "subdomnainDescription": "El subdominio al que su recurso será accesible.", - "resourceRawSettings": "Configuración TCP/UDP", - "resourceRawSettingsDescription": "Configurar cómo se accederá a su recurso a través de TCP/UDP", - "protocol": "Protocolo", - "protocolSelect": "Seleccionar un protocolo", - "resourcePortNumber": "Número de puerto", - "resourcePortNumberDescription": "El número de puerto externo a las solicitudes de proxy.", - "cancel": "Cancelar", - "resourceConfig": "Fragmentos de configuración", - "resourceConfigDescription": "Copia y pega estos fragmentos de configuración para configurar tu recurso TCP/UDP", - "resourceAddEntrypoints": "Traefik: Añadir puntos de entrada", - "resourceExposePorts": "Gerbil: Exponer puertos en Docker Compose", - "resourceLearnRaw": "Aprende cómo configurar los recursos TCP/UDP", - "resourceBack": "Volver a Recursos", - "resourceGoTo": "Ir a Recurso", - "resourceDelete": "Eliminar Recurso", - "resourceDeleteConfirm": "Confirmar Borrar Recurso", - "visibility": "Visibilidad", - "enabled": "Activado", - "disabled": "Deshabilitado", - "general": "General", - "generalSettings": "Configuración General", - "proxy": "Proxy", - "internal": "Interno", - "rules": "Reglas", - "resourceSettingDescription": "Configure la configuración de su recurso", - "resourceSetting": "Ajustes {resourceName}", - "alwaysAllow": "Permitir siempre", - "alwaysDeny": "Denegar siempre", - "passToAuth": "Pasar a Autenticación", - "orgSettingsDescription": "Configurar la configuración general de su organización", - "orgGeneralSettings": "Configuración de la organización", - "orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización", - "saveGeneralSettings": "Guardar ajustes generales", - "saveSettings": "Guardar ajustes", - "orgDangerZone": "Zona de peligro", - "orgDangerZoneDescription": "Una vez que elimines este órgano, no hay vuelta atrás. Por favor, asegúrate de ello.", - "orgDelete": "Eliminar organización", - "orgDeleteConfirm": "Confirmar eliminación de organización", - "orgMessageRemove": "Esta acción es irreversible y eliminará todos los datos asociados.", - "orgMessageConfirm": "Para confirmar, por favor escriba el nombre de la organización a continuación.", - "orgQuestionRemove": "¿Está seguro que desea eliminar la organización {selectedOrg}?", - "orgUpdated": "Organización actualizada", - "orgUpdatedDescription": "La organización ha sido actualizada.", - "orgErrorUpdate": "Error al actualizar la organización", - "orgErrorUpdateMessage": "Se ha producido un error al actualizar la organización.", - "orgErrorFetch": "Error al recuperar organizaciones", - "orgErrorFetchMessage": "Se ha producido un error al listar sus organizaciones", - "orgErrorDelete": "Error al eliminar la organización", - "orgErrorDeleteMessage": "Se ha producido un error al eliminar la organización.", - "orgDeleted": "Organización eliminada", - "orgDeletedMessage": "La organización y sus datos han sido eliminados.", - "orgMissing": "Falta el ID de la organización", - "orgMissingMessage": "No se puede regenerar la invitación sin el ID de la organización.", - "accessUsersManage": "Administrar usuarios", - "accessUsersDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a su organización", - "accessUsersSearch": "Buscar usuarios...", - "accessUserCreate": "Crear usuario", - "accessUserRemove": "Eliminar usuario", - "username": "Usuario", - "identityProvider": "Proveedor de identidad", - "role": "Rol", - "nameRequired": "Se requiere nombre", - "accessRolesManage": "Administrar roles", - "accessRolesDescription": "Configurar roles para administrar el acceso a su organización", - "accessRolesSearch": "Buscar roles...", - "accessRolesAdd": "Añadir rol", - "accessRoleDelete": "Eliminar rol", - "description": "Descripción", - "inviteTitle": "Invitaciones abiertas", - "inviteDescription": "Administra tus invitaciones a otros usuarios", - "inviteSearch": "Buscar invitaciones...", - "minutes": "Minutos", - "hours": "Horas", - "days": "Días", - "weeks": "Semanas", - "months": "Meses", - "years": "Años", - "day": "{count, plural, one {# día} other {# días}}", - "apiKeysTitle": "Información de Clave API", - "apiKeysConfirmCopy2": "Debes confirmar que has copiado la clave API.", - "apiKeysErrorCreate": "Error al crear la clave API", - "apiKeysErrorSetPermission": "Error al establecer permisos", - "apiKeysCreate": "Generar clave API", - "apiKeysCreateDescription": "Generar una nueva clave API para su organización", - "apiKeysGeneralSettings": "Permisos", - "apiKeysGeneralSettingsDescription": "Determinar qué puede hacer esta clave API", - "apiKeysList": "Tu clave API", - "apiKeysSave": "Guarda tu clave API", - "apiKeysSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", - "apiKeysInfo": "Tu clave API es:", - "apiKeysConfirmCopy": "He copiado la clave API", - "generate": "Generar", - "done": "Hecho", - "apiKeysSeeAll": "Ver todas las claves API", - "apiKeysPermissionsErrorLoadingActions": "Error al cargar las acciones clave API", - "apiKeysPermissionsErrorUpdate": "Error al establecer permisos", - "apiKeysPermissionsUpdated": "Permisos actualizados", - "apiKeysPermissionsUpdatedDescription": "Los permisos han sido actualizados.", - "apiKeysPermissionsGeneralSettings": "Permisos", - "apiKeysPermissionsGeneralSettingsDescription": "Determinar qué puede hacer esta clave API", - "apiKeysPermissionsSave": "Guardar permisos", - "apiKeysPermissionsTitle": "Permisos", - "apiKeys": "Claves API", - "searchApiKeys": "Buscar claves API...", - "apiKeysAdd": "Generar clave API", - "apiKeysErrorDelete": "Error al eliminar la clave API", - "apiKeysErrorDeleteMessage": "Error al eliminar la clave API", - "apiKeysQuestionRemove": "¿Está seguro de que desea eliminar la clave de API {selectedApiKey} de la organización?", - "apiKeysMessageRemove": "Una vez eliminada, la clave API ya no podrá ser utilizada.", - "apiKeysMessageConfirm": "Para confirmar, por favor escriba el nombre de la clave API a continuación.", - "apiKeysDeleteConfirm": "Confirmar Borrar Clave API", - "apiKeysDelete": "Borrar Clave API", - "apiKeysManage": "Administrar claves API", - "apiKeysDescription": "Las claves API se utilizan para autenticar con la API de integración", - "apiKeysSettings": "Ajustes {apiKeyName}", - "userTitle": "Administrar todos los usuarios", - "userDescription": "Ver y administrar todos los usuarios en el sistema", - "userAbount": "Acerca de Gestión de Usuarios", - "userAbountDescription": "Esta tabla muestra todos los objetos de usuario root en el sistema. Cada usuario puede pertenecer a varias organizaciones. Eliminar un usuario de una organización no elimina su objeto de usuario root - permanecerán en el sistema. Para eliminar completamente un usuario del sistema, debe eliminar su objeto de usuario root usando la acción de borrar en esta tabla.", - "userServer": "Usuarios del servidor", - "userSearch": "Buscar usuarios del servidor...", - "userErrorDelete": "Error al eliminar el usuario", - "userDeleteConfirm": "Confirmar Borrar Usuario", - "userDeleteServer": "Eliminar usuario del servidor", - "userMessageRemove": "El usuario será eliminado de todas las organizaciones y será eliminado completamente del servidor.", - "userMessageConfirm": "Para confirmar, por favor escriba el nombre del usuario a continuación.", - "userQuestionRemove": "¿Está seguro que desea eliminar permanentemente {selectedUser} del servidor?", - "licenseKey": "Clave de licencia", - "valid": "Válido", - "numberOfSites": "Número de sitios", - "licenseKeySearch": "Buscar claves de licencia...", - "licenseKeyAdd": "Añadir clave de licencia", - "type": "Tipo", - "licenseKeyRequired": "La clave de licencia es necesaria", - "licenseTermsAgree": "Debe aceptar los términos de la licencia", - "licenseErrorKeyLoad": "Error al cargar las claves de licencia", - "licenseErrorKeyLoadDescription": "Se ha producido un error al cargar las claves de licencia.", - "licenseErrorKeyDelete": "Error al eliminar la clave de licencia", - "licenseErrorKeyDeleteDescription": "Se ha producido un error al eliminar la clave de licencia.", - "licenseKeyDeleted": "Clave de licencia eliminada", - "licenseKeyDeletedDescription": "La clave de licencia ha sido eliminada.", - "licenseErrorKeyActivate": "Error al activar la clave de licencia", - "licenseErrorKeyActivateDescription": "Se ha producido un error al activar la clave de licencia.", - "licenseAbout": "Acerca de la licencia", - "communityEdition": "Edición comunitaria", - "licenseAboutDescription": "Esto es para usuarios empresariales y empresariales que utilizan Pangolin en un entorno comercial. Si estás usando Pangolin para uso personal, puedes ignorar esta sección.", - "licenseKeyActivated": "Clave de licencia activada", - "licenseKeyActivatedDescription": "La clave de licencia se ha activado correctamente.", - "licenseErrorKeyRecheck": "Error al revisar las claves de licencia", - "licenseErrorKeyRecheckDescription": "Se ha producido un error al revisar las claves de licencia.", - "licenseErrorKeyRechecked": "Claves de licencia remarcadas", - "licenseErrorKeyRecheckedDescription": "Todas las claves de licencia han sido revisadas", - "licenseActivateKey": "Activar clave de licencia", - "licenseActivateKeyDescription": "Introduzca una clave de licencia para activarla.", - "licenseActivate": "Activar licencia", - "licenseAgreement": "Al marcar esta casilla, confirma que ha leído y aceptado los términos de licencia correspondientes al nivel asociado con su clave de licencia.", - "fossorialLicense": "Ver Términos de suscripción y licencia comercial", - "licenseMessageRemove": "Esto eliminará la clave de licencia y todos los permisos asociados otorgados por ella.", - "licenseMessageConfirm": "Para confirmar, por favor escriba la clave de licencia a continuación.", - "licenseQuestionRemove": "¿Está seguro que desea eliminar la clave de licencia {selectedKey}?", - "licenseKeyDelete": "Eliminar clave de licencia", - "licenseKeyDeleteConfirm": "Confirmar eliminar clave de licencia", - "licenseTitle": "Administrar estado de licencia", - "licenseTitleDescription": "Ver y administrar claves de licencia en el sistema", - "licenseHost": "Licencia de host", - "licenseHostDescription": "Administrar la clave de licencia principal para el host.", - "licensedNot": "Sin licencia", - "hostId": "ID del Host", - "licenseReckeckAll": "Revisar todas las claves", - "licenseSiteUsage": "Uso de Sitios", - "licenseSiteUsageDecsription": "Ver el número de sitios que utilizan esta licencia.", - "licenseNoSiteLimit": "No hay límite en el número de sitios que utilizan un host sin licencia.", - "licensePurchase": "Comprar Licencia", - "licensePurchaseSites": "Comprar sitios adicionales", - "licenseSitesUsedMax": "{usedSites} de {maxSites} sitios usados", - "licenseSitesUsed": "{count, plural, =0 {# sitios} one {# sitio} other {# sitios}} en el sistema.", - "licensePurchaseDescription": "Elige cuántos sitios quieres {selectedMode, select, license {compra una licencia para. Siempre puedes añadir más sitios más tarde.} other {añadir a tu licencia existente.}}", - "licenseFee": "Tarifa de licencia", - "licensePriceSite": "Precio por sitio", - "total": "Total", - "licenseContinuePayment": "Continuar con el pago", - "pricingPage": "página de precios", - "pricingPortal": "Ver Portal de Compra", - "licensePricingPage": "Para obtener los precios y descuentos más actualizados, por favor visite el ", - "invite": "Invitaciones", - "inviteRegenerate": "Regenerar invitación", - "inviteRegenerateDescription": "Revocar invitación anterior y crear una nueva", - "inviteRemove": "Eliminar invitación", - "inviteRemoveError": "Error al eliminar la invitación", - "inviteRemoveErrorDescription": "Ocurrió un error mientras se eliminaba la invitación.", - "inviteRemoved": "Invitación eliminada", - "inviteRemovedDescription": "La invitación para {email} ha sido eliminada.", - "inviteQuestionRemove": "¿Está seguro de que desea eliminar la invitación {email}?", - "inviteMessageRemove": "Una vez eliminada, esta invitación ya no será válida. Siempre puede volver a invitar al usuario más tarde.", - "inviteMessageConfirm": "Para confirmar, por favor escriba la dirección de correo electrónico de la invitación a continuación.", - "inviteQuestionRegenerate": "¿Estás seguro de que quieres regenerar la invitación para {email}? Esto revocará la invitación anterior.", - "inviteRemoveConfirm": "Confirmar eliminación de invitación", - "inviteRegenerated": "Invitación Regenerada", - "inviteSent": "Se ha enviado una nueva invitación a {email}.", - "inviteSentEmail": "Enviar notificación por correo electrónico al usuario", - "inviteGenerate": "Se ha generado una nueva invitación para {email}.", - "inviteDuplicateError": "Invitación duplicada", - "inviteDuplicateErrorDescription": "Ya existe una invitación para este usuario.", - "inviteRateLimitError": "Límite de tasa excedido", - "inviteRateLimitErrorDescription": "Has superado el límite de 3 regeneraciones por hora. Inténtalo de nuevo más tarde.", - "inviteRegenerateError": "No se pudo regenerar la invitación", - "inviteRegenerateErrorDescription": "Se ha producido un error al regenerar la invitación.", - "inviteValidityPeriod": "Periodo de validez", - "inviteValidityPeriodSelect": "Seleccionar período de validez", - "inviteRegenerateMessage": "La invitación ha sido regenerada. El usuario debe acceder al enlace de abajo para aceptar la invitación.", - "inviteRegenerateButton": "Regenerar", - "expiresAt": "Caduca el", - "accessRoleUnknown": "Rol desconocido", - "placeholder": "Marcador de posición", - "userErrorOrgRemove": "Error al eliminar el usuario", - "userErrorOrgRemoveDescription": "Ocurrió un error mientras se eliminaba el usuario.", - "userOrgRemoved": "Usuario eliminado", - "userOrgRemovedDescription": "El usuario {email} ha sido eliminado de la organización.", - "userQuestionOrgRemove": "¿Estás seguro de que quieres eliminar {email} de la organización?", - "userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.", - "userMessageOrgConfirm": "Para confirmar, por favor escriba el nombre del usuario a continuación.", - "userRemoveOrgConfirm": "Confirmar eliminar usuario", - "userRemoveOrg": "Eliminar usuario de la organización", - "users": "Usuarios", - "accessRoleMember": "Miembro", - "accessRoleOwner": "Propietario", - "userConfirmed": "Confirmada", - "idpNameInternal": "Interno", - "emailInvalid": "Dirección de correo inválida", - "inviteValidityDuration": "Por favor, seleccione una duración", - "accessRoleSelectPlease": "Por favor, seleccione un rol", - "usernameRequired": "Nombre de usuario requerido", - "idpSelectPlease": "Por favor, seleccione un proveedor de identidad", - "idpGenericOidc": "Proveedor OAuth2/OIDC genérico.", - "accessRoleErrorFetch": "Error al recuperar roles", - "accessRoleErrorFetchDescription": "Se ha producido un error al recuperar los roles", - "idpErrorFetch": "Error al recuperar proveedores de identidad", - "idpErrorFetchDescription": "Se ha producido un error al recuperar proveedores de identidad", - "userErrorExists": "El usuario ya existe", - "userErrorExistsDescription": "Este usuario ya es miembro de la organización.", - "inviteError": "Error al invitar al usuario", - "inviteErrorDescription": "Ocurrió un error mientras se invitaba al usuario", - "userInvited": "Usuario invitado", - "userInvitedDescription": "El usuario ha sido invitado con éxito.", - "userErrorCreate": "Error al crear el usuario", - "userErrorCreateDescription": "Se ha producido un error al crear el usuario", - "userCreated": "Usuario creado", - "userCreatedDescription": "El usuario se ha creado correctamente.", - "userTypeInternal": "Usuario interno", - "userTypeInternalDescription": "Invita a un usuario a unirse a tu organización directamente.", - "userTypeExternal": "Usuario externo", - "userTypeExternalDescription": "Crear un usuario con un proveedor de identidad externo.", - "accessUserCreateDescription": "Siga los pasos siguientes para crear un nuevo usuario", - "userSeeAll": "Ver todos los usuarios", - "userTypeTitle": "Tipo de usuario", - "userTypeDescription": "Determina cómo quieres crear el usuario", - "userSettings": "Información del usuario", - "userSettingsDescription": "Introduzca los detalles del nuevo usuario", - "inviteEmailSent": "Enviar correo de invitación al usuario", - "inviteValid": "Válido para", - "selectDuration": "Seleccionar duración", - "accessRoleSelect": "Seleccionar rol", - "inviteEmailSentDescription": "Se ha enviado un correo electrónico al usuario con el siguiente enlace de acceso. Debe acceder al enlace para aceptar la invitación.", - "inviteSentDescription": "El usuario ha sido invitado. Debe acceder al enlace de abajo para aceptar la invitación.", - "inviteExpiresIn": "La invitación expirará en {days, plural, one {# día} other {# días}}.", - "idpTitle": "Proveedor de identidad", - "idpSelect": "Seleccione el proveedor de identidad para el usuario externo", - "idpNotConfigured": "No hay proveedores de identidad configurados. Por favor, configure un proveedor de identidad antes de crear usuarios externos.", - "usernameUniq": "Esto debe coincidir con el nombre de usuario único que existe en el proveedor de identidad seleccionado.", - "emailOptional": "Email (opcional)", - "nameOptional": "Nombre (opcional)", - "accessControls": "Controles de acceso", - "userDescription2": "Administrar la configuración de este usuario", - "accessRoleErrorAdd": "No se pudo agregar el usuario al rol", - "accessRoleErrorAddDescription": "Ocurrió un error mientras se añadía el usuario al rol.", - "userSaved": "Usuario guardado", - "userSavedDescription": "El usuario ha sido actualizado.", - "autoProvisioned": "Auto asegurado", - "autoProvisionedDescription": "Permitir a este usuario ser administrado automáticamente por el proveedor de identidad", - "accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización", - "accessControlsSubmit": "Guardar controles de acceso", - "roles": "Roles", - "accessUsersRoles": "Administrar usuarios y roles", - "accessUsersRolesDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a su organización", - "key": "Clave", - "createdAt": "Creado el", - "proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.", - "proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.", - "proxyEnableSSL": "Activar SSL", - "proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.", - "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", - "siteErrorFetchDescription": "Se ha producido un error al recuperar el recurso", - "targetErrorDuplicate": "Objetivo duplicado", - "targetErrorDuplicateDescription": "Ya existe un objetivo con estos ajustes", - "targetWireGuardErrorInvalidIp": "IP de destino no válida", - "targetWireGuardErrorInvalidIpDescription": "La IP de destino debe estar dentro de la subred del sitio", - "targetsUpdated": "Objetivos actualizados", - "targetsUpdatedDescription": "Objetivos y ajustes actualizados correctamente", - "targetsErrorUpdate": "Error al actualizar los objetivos", - "targetsErrorUpdateDescription": "Se ha producido un error al actualizar los objetivos", - "targetTlsUpdate": "Ajustes TLS actualizados", - "targetTlsUpdateDescription": "La configuración de TLS se ha actualizado correctamente", - "targetErrorTlsUpdate": "Error al actualizar los ajustes de TLS", - "targetErrorTlsUpdateDescription": "Ocurrió un error mientras se actualizaban los ajustes de TLS", - "proxyUpdated": "Configuración del proxy actualizada", - "proxyUpdatedDescription": "La configuración del proxy se ha actualizado correctamente", - "proxyErrorUpdate": "Error al actualizar la configuración del proxy", - "proxyErrorUpdateDescription": "Se ha producido un error al actualizar la configuración del proxy", - "targetAddr": "IP / Nombre del host", - "targetPort": "Puerto", - "targetProtocol": "Protocolo", - "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", - "targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.", - "targetTlsSubmit": "Guardar ajustes", - "targets": "Configuración de objetivos", - "targetsDescription": "Configurar objetivos para enrutar tráfico a sus servicios", - "targetStickySessions": "Activar Sesiones Pegadas", - "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", - "methodSelect": "Seleccionar método", - "targetSubmit": "Añadir destino", - "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", - "proxyCustomHeaderDescription": "La cabecera del host a establecer cuando se realizan peticiones de reemplazo. Deje en blanco para usar el valor predeterminado.", - "proxyAdditionalSubmit": "Guardar ajustes de proxy", - "subnetMaskErrorInvalid": "Máscara de subred inválida. Debe estar entre 0 y 32.", - "ipAddressErrorInvalidFormat": "Formato de dirección IP inválido", - "ipAddressErrorInvalidOctet": "Octet de dirección IP no válido", - "path": "Ruta", - "matchPath": "Coincidir ruta", - "ipAddressRange": "Rango IP", - "rulesErrorFetch": "Error al obtener las reglas", - "rulesErrorFetchDescription": "Se ha producido un error al recuperar las reglas", - "rulesErrorDuplicate": "Duplicar regla", - "rulesErrorDuplicateDescription": "Ya existe una regla con estos ajustes", - "rulesErrorInvalidIpAddressRange": "CIDR inválido", - "rulesErrorInvalidIpAddressRangeDescription": "Por favor, introduzca un valor CIDR válido", - "rulesErrorInvalidUrl": "Ruta URL inválida", - "rulesErrorInvalidUrlDescription": "Por favor, introduzca un valor de ruta de URL válido", - "rulesErrorInvalidIpAddress": "IP inválida", - "rulesErrorInvalidIpAddressDescription": "Por favor, introduzca una dirección IP válida", - "rulesErrorUpdate": "Error al actualizar las reglas", - "rulesErrorUpdateDescription": "Se ha producido un error al actualizar las reglas", - "rulesUpdated": "Activar Reglas", - "rulesUpdatedDescription": "La evaluación de la regla ha sido actualizada", - "rulesMatchIpAddressRangeDescription": "Introduzca una dirección en formato CIDR (por ejemplo, 103.21.244.0/22)", - "rulesMatchIpAddress": "Introduzca una dirección IP (por ejemplo, 103.21.244.12)", - "rulesMatchUrl": "Introduzca una ruta URL o patrón (por ej., /api/v1/todos o /api/v1/*)", - "rulesErrorInvalidPriority": "Prioridad inválida", - "rulesErrorInvalidPriorityDescription": "Por favor, introduzca una prioridad válida", - "rulesErrorDuplicatePriority": "Prioridades duplicadas", - "rulesErrorDuplicatePriorityDescription": "Por favor, introduzca prioridades únicas", - "ruleUpdated": "Reglas actualizadas", - "ruleUpdatedDescription": "Reglas actualizadas correctamente", - "ruleErrorUpdate": "Operación fallida", - "ruleErrorUpdateDescription": "Se ha producido un error durante la operación de guardado", - "rulesPriority": "Prioridad", - "rulesAction": "Accin", - "rulesMatchType": "Tipo de partida", - "value": "Valor", - "rulesAbout": "Sobre Reglas", - "rulesAboutDescription": "Las reglas le permiten controlar el acceso a su recurso basado en un conjunto de criterios. Puede crear reglas para permitir o denegar el acceso basándose en la dirección IP o ruta de la URL.", - "rulesActions": "Acciones", - "rulesActionAlwaysAllow": "Permitir siempre: pasar todos los métodos de autenticación", - "rulesActionAlwaysDeny": "Denegar siempre: Bloquear todas las peticiones; no se puede intentar autenticación", - "rulesActionPassToAuth": "Pasar a Autenticación: Permitir que se intenten los métodos de autenticación", - "rulesMatchCriteria": "Criterios coincidentes", - "rulesMatchCriteriaIpAddress": "Coincidir con una dirección IP específica", - "rulesMatchCriteriaIpAddressRange": "Coincide con un rango de direcciones IP en notación CIDR", - "rulesMatchCriteriaUrl": "Coincidir con una ruta de URL o patrón", - "rulesEnable": "Activar Reglas", - "rulesEnableDescription": "Activar o desactivar la evaluación de reglas para este recurso", - "rulesResource": "Configuración de reglas de recursos", - "rulesResourceDescription": "Configurar reglas para controlar el acceso a su recurso", - "ruleSubmit": "Añadir Regla", - "rulesNoOne": "No hay reglas. Agregue una regla usando el formulario.", - "rulesOrder": "Las reglas son evaluadas por prioridad en orden ascendente.", - "rulesSubmit": "Guardar Reglas", - "resourceErrorCreate": "Error al crear recurso", - "resourceErrorCreateDescription": "Se ha producido un error al crear el recurso", - "resourceErrorCreateMessage": "Error al crear el recurso:", - "resourceErrorCreateMessageDescription": "Se ha producido un error inesperado", - "sitesErrorFetch": "Error obteniendo sitios", - "sitesErrorFetchDescription": "Se ha producido un error al recuperar los sitios", - "domainsErrorFetch": "Error obteniendo dominios", - "domainsErrorFetchDescription": "Se ha producido un error al recuperar los dominios", - "none": "Ninguna", - "unknown": "Desconocido", - "resources": "Recursos", - "resourcesDescription": "Los recursos son proxies para aplicaciones que se ejecutan en su red privada. Cree un recurso para cualquier servicio HTTP/HTTPS o TCP/UDP crudo en su red privada. Cada recurso debe estar conectado a un sitio para permitir una conectividad privada y segura a través de un túnel encriptado de WireGuard.", - "resourcesWireGuardConnect": "Conectividad segura con cifrado de Wirex Guard", - "resourcesMultipleAuthenticationMethods": "Configurar múltiples métodos de autenticación", - "resourcesUsersRolesAccess": "Control de acceso basado en usuarios y roles", - "resourcesErrorUpdate": "Error al cambiar el recurso", - "resourcesErrorUpdateDescription": "Se ha producido un error al actualizar el recurso", - "access": "Acceder", - "shareLink": "{resource} Compartir Enlace", - "resourceSelect": "Seleccionar recurso", - "shareLinks": "Compartir enlaces", - "share": "Enlaces compartibles", - "shareDescription2": "Crea enlaces compartidos con tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puede configurar la duración de caducidad del enlace cuando cree uno.", - "shareEasyCreate": "Fácil de crear y compartir", - "shareConfigurableExpirationDuration": "Duración de caducidad configurable", - "shareSecureAndRevocable": "Seguro y revocable", - "nameMin": "El nombre debe tener al menos caracteres {len}.", - "nameMax": "El nombre no debe tener más de {len} caracteres.", - "sitesConfirmCopy": "Por favor, confirme que ha copiado la configuración.", - "unknownCommand": "Comando desconocido", - "newtErrorFetchReleases": "No se pudo obtener la información del lanzamiento: {err}", - "newtErrorFetchLatest": "Error obteniendo la última versión: {err}", - "newtEndpoint": "Punto final de Newt", - "newtId": "ID de Newt", - "newtSecretKey": "Clave secreta de Newt", - "architecture": "Arquitectura", - "sites": "Sitios", - "siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a tus recursos internos usando la IP de compañeros.", - "siteWgCompatibleAllClients": "Compatible con todos los clientes de Wirex Guard", - "siteWgManualConfigurationRequired": "Configuración manual requerida", - "userErrorNotAdminOrOwner": "El usuario no es un administrador o propietario", - "pangolinSettings": "Ajustes - Pangolin", - "accessRoleYour": "Tu rol:", - "accessRoleSelect2": "Seleccione un rol", - "accessUserSelect": "Seleccione un usuario", - "otpEmailEnter": "Escribe un email", - "otpEmailEnterDescription": "Pulse Enter para añadir un correo electrónico después de teclearlo en el campo de entrada.", - "otpEmailErrorInvalid": "Dirección de correo electrónico no válida. El comodín (*) debe ser la parte local completa.", - "otpEmailSmtpRequired": "SMTP Requerido", - "otpEmailSmtpRequiredDescription": "SMTP debe estar habilitado en el servidor para usar autenticación de contraseña de una sola vez.", - "otpEmailTitle": "Contraseñas de una sola vez", - "otpEmailTitleDescription": "Requiere autenticación por correo electrónico para acceso a recursos", - "otpEmailWhitelist": "Lista blanca de correo", - "otpEmailWhitelistList": "Correos en la lista blanca", - "otpEmailWhitelistListDescription": "Sólo los usuarios con estas direcciones de correo electrónico podrán acceder a este recurso. Se les pedirá que introduzcan una contraseña de una sola vez enviada a su correo electrónico. Los comodines (*@ejemplo.com) pueden utilizarse para permitir cualquier dirección de correo electrónico de un dominio.", - "otpEmailWhitelistSave": "Guardar lista blanca", - "passwordAdd": "Añadir contraseña", - "passwordRemove": "Eliminar contraseña", - "pincodeAdd": "Añadir código PIN", - "pincodeRemove": "Eliminar código PIN", - "resourceAuthMethods": "Métodos de autenticación", - "resourceAuthMethodsDescriptions": "Permitir el acceso al recurso a través de métodos de autenticación adicionales", - "resourceAuthSettingsSave": "Guardado correctamente", - "resourceAuthSettingsSaveDescription": "Se han guardado los ajustes de autenticación", - "resourceErrorAuthFetch": "Error al recuperar datos", - "resourceErrorAuthFetchDescription": "Se ha producido un error al recuperar los datos", - "resourceErrorPasswordRemove": "Error al eliminar la contraseña del recurso", - "resourceErrorPasswordRemoveDescription": "Se ha producido un error al eliminar la contraseña del recurso", - "resourceErrorPasswordSetup": "Error al establecer la contraseña del recurso", - "resourceErrorPasswordSetupDescription": "Se ha producido un error al establecer la contraseña del recurso", - "resourceErrorPincodeRemove": "Error al eliminar el código pin del recurso", - "resourceErrorPincodeRemoveDescription": "Ocurrió un error mientras se eliminaba el código pin del recurso", - "resourceErrorPincodeSetup": "Error al establecer el código PIN del recurso", - "resourceErrorPincodeSetupDescription": "Se ha producido un error al establecer el código PIN del recurso", - "resourceErrorUsersRolesSave": "Error al establecer roles", - "resourceErrorUsersRolesSaveDescription": "Se ha producido un error al establecer los roles", - "resourceErrorWhitelistSave": "Error al guardar la lista blanca", - "resourceErrorWhitelistSaveDescription": "Ocurrió un error mientras se guardaba la lista blanca", - "resourcePasswordSubmit": "Activar la protección de contraseña", - "resourcePasswordProtection": "Protección de contraseña {status}", - "resourcePasswordRemove": "Contraseña de recurso eliminada", - "resourcePasswordRemoveDescription": "La contraseña del recurso se ha eliminado correctamente", - "resourcePasswordSetup": "Contraseña de recurso establecida", - "resourcePasswordSetupDescription": "La contraseña del recurso se ha establecido correctamente", - "resourcePasswordSetupTitle": "Establecer contraseña", - "resourcePasswordSetupTitleDescription": "Establecer una contraseña para proteger este recurso", - "resourcePincode": "Código PIN", - "resourcePincodeSubmit": "Activar protección de código PIN", - "resourcePincodeProtection": "Protección del código PIN {status}", - "resourcePincodeRemove": "Código del recurso eliminado", - "resourcePincodeRemoveDescription": "La contraseña del recurso se ha eliminado correctamente", - "resourcePincodeSetup": "Código PIN del recurso establecido", - "resourcePincodeSetupDescription": "El código del recurso se ha establecido correctamente", - "resourcePincodeSetupTitle": "Definir Pincode", - "resourcePincodeSetupTitleDescription": "Establecer un pincode para proteger este recurso", - "resourceRoleDescription": "Los administradores siempre pueden acceder a este recurso.", - "resourceUsersRoles": "Usuarios y roles", - "resourceUsersRolesDescription": "Configurar qué usuarios y roles pueden visitar este recurso", - "resourceUsersRolesSubmit": "Guardar usuarios y roles", - "resourceWhitelistSave": "Guardado correctamente", - "resourceWhitelistSaveDescription": "Se han guardado los ajustes de la lista blanca", - "ssoUse": "Usar Plataforma SSO", - "ssoUseDescription": "Los usuarios existentes sólo tendrán que iniciar sesión una vez para todos los recursos que tengan esto habilitado.", - "proxyErrorInvalidPort": "Número de puerto inválido", - "subdomainErrorInvalid": "Subdominio inválido", - "domainErrorFetch": "Error obteniendo dominios", - "domainErrorFetchDescription": "Se ha producido un error al recuperar los dominios", - "resourceErrorUpdate": "Error al actualizar el recurso", - "resourceErrorUpdateDescription": "Se ha producido un error al actualizar el recurso", - "resourceUpdated": "Recurso actualizado", - "resourceUpdatedDescription": "El recurso se ha actualizado correctamente", - "resourceErrorTransfer": "Error al transferir el recurso", - "resourceErrorTransferDescription": "Se ha producido un error al transferir el recurso", - "resourceTransferred": "Recurso transferido", - "resourceTransferredDescription": "El recurso ha sido transferido con éxito", - "resourceErrorToggle": "Error al cambiar el recurso", - "resourceErrorToggleDescription": "Se ha producido un error al actualizar el recurso", - "resourceVisibilityTitle": "Visibilidad", - "resourceVisibilityTitleDescription": "Activar o desactivar completamente la visibilidad de los recursos", - "resourceGeneral": "Configuración General", - "resourceGeneralDescription": "Configurar la configuración general de este recurso", - "resourceEnable": "Activar recurso", - "resourceTransfer": "Transferir recursos", - "resourceTransferDescription": "Transferir este recurso a un sitio diferente", - "resourceTransferSubmit": "Transferir recursos", - "siteDestination": "Sitio de destino", - "searchSites": "Buscar sitios", - "accessRoleCreate": "Crear rol", - "accessRoleCreateDescription": "Crear un nuevo rol para agrupar usuarios y administrar sus permisos.", - "accessRoleCreateSubmit": "Crear rol", - "accessRoleCreated": "Rol creado", - "accessRoleCreatedDescription": "El rol se ha creado correctamente.", - "accessRoleErrorCreate": "Error al crear el rol", - "accessRoleErrorCreateDescription": "Se ha producido un error al crear el rol.", - "accessRoleErrorNewRequired": "Se requiere un nuevo rol", - "accessRoleErrorRemove": "Error al eliminar el rol", - "accessRoleErrorRemoveDescription": "Ocurrió un error mientras se eliminaba el rol.", - "accessRoleName": "Nombre del Rol", - "accessRoleQuestionRemove": "Estás a punto de eliminar el rol {name} . No puedes deshacer esta acción.", - "accessRoleRemove": "Quitar rol", - "accessRoleRemoveDescription": "Eliminar un rol de la organización", - "accessRoleRemoveSubmit": "Quitar rol", - "accessRoleRemoved": "Rol eliminado", - "accessRoleRemovedDescription": "El rol se ha eliminado correctamente.", - "accessRoleRequiredRemove": "Antes de eliminar este rol, seleccione un nuevo rol al que transferir miembros existentes.", - "manage": "Gestionar", - "sitesNotFound": "Sitios no encontrados.", - "pangolinServerAdmin": "Admin Servidor - Pangolin", - "licenseTierProfessional": "Licencia profesional", - "licenseTierEnterprise": "Licencia Enterprise", - "licenseTierPersonal": "Personal License", - "licensed": "Licenciado", - "yes": "Sí", - "no": "Nu", - "sitesAdditional": "Sitios adicionales", - "licenseKeys": "Claves de licencia", - "sitestCountDecrease": "Reducir el número de sitios", - "sitestCountIncrease": "Aumentar el número de sitios", - "idpManage": "Administrar proveedores de identidad", - "idpManageDescription": "Ver y administrar proveedores de identidad en el sistema", - "idpDeletedDescription": "Proveedor de identidad eliminado correctamente", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "¿Está seguro que desea eliminar permanentemente el proveedor de identidad {name}?", - "idpMessageRemove": "Esto eliminará el proveedor de identidad y todas las configuraciones asociadas. Los usuarios que se autentifiquen a través de este proveedor ya no podrán iniciar sesión.", - "idpMessageConfirm": "Para confirmar, por favor escriba el nombre del proveedor de identidad a continuación.", - "idpConfirmDelete": "Confirmar eliminar proveedor de identidad", - "idpDelete": "Eliminar proveedor de identidad", - "idp": "Proveedores de identidad", - "idpSearch": "Buscar proveedores de identidad...", - "idpAdd": "Añadir proveedor de identidad", - "idpClientIdRequired": "Se requiere ID de cliente.", - "idpClientSecretRequired": "El secreto del cliente es obligatorio.", - "idpErrorAuthUrlInvalid": "La URL de autenticación debe ser una URL válida.", - "idpErrorTokenUrlInvalid": "La URL del token debe ser una URL válida.", - "idpPathRequired": "La ruta identificadora es requerida.", - "idpScopeRequired": "Se requiere alcance.", - "idpOidcDescription": "Configurar un proveedor de identidad OpenID Connect", - "idpCreatedDescription": "Proveedor de identidad creado correctamente", - "idpCreate": "Crear proveedor de identidad", - "idpCreateDescription": "Configurar un nuevo proveedor de identidad para la autenticación de usuario", - "idpSeeAll": "Ver todos los proveedores de identidad", - "idpSettingsDescription": "Configure la información básica para su proveedor de identidad", - "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": "EE", - "idpType": "Tipo de proveedor", - "idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar", - "idpOidcConfigure": "Configuración OAuth2/OIDC", - "idpOidcConfigureDescription": "Configurar los puntos finales y credenciales del proveedor OAuth2/OIDC", - "idpClientId": "ID de cliente", - "idpClientIdDescription": "El ID del cliente OAuth2 de su proveedor de identidad", - "idpClientSecret": "Cliente secreto", - "idpClientSecretDescription": "El secreto del cliente OAuth2 de su proveedor de identidad", - "idpAuthUrl": "URL de autorización", - "idpAuthUrlDescription": "La URL final de autorización de OAuth2", - "idpTokenUrl": "URL del token", - "idpTokenUrlDescription": "La URL del endpoint del token OAuth2", - "idpOidcConfigureAlert": "Información importante", - "idpOidcConfigureAlertDescription": "Después de crear el proveedor de identidad, necesitará configurar la URL de callback en la configuración de su proveedor de identidad. La URL de devolución de llamada se proporcionará después de la creación exitosa.", - "idpToken": "Configuración del token", - "idpTokenDescription": "Configurar cómo extraer la información del usuario del token de ID", - "idpJmespathAbout": "Acerca de JMESPath", - "idpJmespathAboutDescription": "Las siguientes rutas utilizan la sintaxis JMESPath para extraer valores del token ID.", - "idpJmespathAboutDescriptionLink": "Más información sobre JMESPath", - "idpJmespathLabel": "Ruta del identificador", - "idpJmespathLabelDescription": "La ruta al identificador de usuario en el token de ID", - "idpJmespathEmailPathOptional": "Ruta de correo (opcional)", - "idpJmespathEmailPathOptionalDescription": "La ruta al correo electrónico del usuario en el token de ID", - "idpJmespathNamePathOptional": "Ruta del nombre (opcional)", - "idpJmespathNamePathOptionalDescription": "La ruta al nombre del usuario en el token de ID", - "idpOidcConfigureScopes": "Ámbitos", - "idpOidcConfigureScopesDescription": "Lista separada por espacios de los ámbitos OAuth2 a solicitar", - "idpSubmit": "Crear proveedor de identidad", - "orgPolicies": "Políticas de organización", - "idpSettings": "Ajustes {idpName}", - "idpCreateSettingsDescription": "Configurar la configuración de su proveedor de identidad", - "roleMapping": "Mapeo de Rol", - "orgMapping": "Mapeo de organización", - "orgPoliciesSearch": "Buscar políticas de organización...", - "orgPoliciesAdd": "Añadir Política de Organización", - "orgRequired": "La organización es obligatoria", - "error": "Error", - "success": "Éxito", - "orgPolicyAddedDescription": "Política añadida correctamente", - "orgPolicyUpdatedDescription": "Política actualizada correctamente", - "orgPolicyDeletedDescription": "Política eliminada correctamente", - "defaultMappingsUpdatedDescription": "Mapeos por defecto actualizados correctamente", - "orgPoliciesAbout": "Acerca de políticas de organización", - "orgPoliciesAboutDescription": "Las políticas de la organización se utilizan para controlar el acceso a las organizaciones basándose en el token de identificación del usuario. Puede especificar expresiones JMESPath para extraer información de rol y organización del token de identificación.", - "orgPoliciesAboutDescriptionLink": "Vea la documentación, para más información.", - "defaultMappingsOptional": "Mapeo por defecto (opcional)", - "defaultMappingsOptionalDescription": "Los mapeos por defecto se utilizan cuando no hay una política de organización definida para una organización. Puede especificar las asignaciones predeterminadas de rol y organización a las que volver aquí.", - "defaultMappingsRole": "Mapeo de Rol por defecto", - "defaultMappingsRoleDescription": "El resultado de esta expresión debe devolver el nombre del rol tal y como se define en la organización como una cadena.", - "defaultMappingsOrg": "Mapeo de organización por defecto", - "defaultMappingsOrgDescription": "Esta expresión debe devolver el ID de org o verdadero para que el usuario pueda acceder a la organización.", - "defaultMappingsSubmit": "Guardar asignaciones por defecto", - "orgPoliciesEdit": "Editar Política de Organización", - "org": "Organización", - "orgSelect": "Seleccionar organización", - "orgSearch": "Buscar org", - "orgNotFound": "No se encontró org.", - "roleMappingPathOptional": "Ruta de Mapeo de Rol (opcional)", - "orgMappingPathOptional": "Ruta de mapeo de organización (opcional)", - "orgPolicyUpdate": "Actualizar política", - "orgPolicyAdd": "Añadir Política", - "orgPolicyConfig": "Configurar acceso para una organización", - "idpUpdatedDescription": "Proveedor de identidad actualizado correctamente", - "redirectUrl": "URL de redirección", - "redirectUrlAbout": "Acerca de la URL de redirección", - "redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración de tu proveedor de identidad.", - "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:", - "emailVerified": "¡Correo electrónico verificado con éxito! Redirigiendo...", - "verificationCodeErrorResend": "Error al reenviar el código de verificación:", - "verificationCodeResend": "Código de verificación reenviado", - "verificationCodeResendDescription": "Hemos reenviado un código de verificación a tu dirección de correo electrónico. Por favor, comprueba tu bandeja de entrada.", - "emailVerify": "Verificar Email", - "emailVerifyDescription": "Introduzca el código de verificación enviado a su dirección de correo electrónico.", - "verificationCode": "Código de verificación", - "verificationCodeEmailSent": "Hemos enviado un código de verificación a tu dirección de correo electrónico.", - "submit": "Enviar", - "emailVerifyResendProgress": "Reenviando...", - "emailVerifyResend": "¿No has recibido un código? Haz clic aquí para reenviar", - "passwordNotMatch": "Las contraseñas no coinciden", - "signupError": "Se ha producido un error al registrarse", - "pangolinLogoAlt": "Logo de Pangolin", - "inviteAlready": "¡Parece que has sido invitado!", - "inviteAlreadyDescription": "Para aceptar la invitación, debes iniciar sesión o crear una cuenta.", - "signupQuestion": "¿Ya tienes una cuenta?", - "login": "Iniciar sesión", - "resourceNotFound": "Recurso no encontrado", - "resourceNotFoundDescription": "El recurso al que intentas acceder no existe.", - "pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos", - "pincodeRequirementsChars": "El PIN sólo debe contener números", - "passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter", - "passwordRequirementsTitle": "Requisitos de la contraseña:", - "passwordRequirementLength": "Al menos 8 caracteres de largo", - "passwordRequirementUppercase": "Al menos una letra mayúscula", - "passwordRequirementLowercase": "Al menos una letra minúscula", - "passwordRequirementNumber": "Al menos un número", - "passwordRequirementSpecial": "Al menos un carácter especial", - "passwordRequirementsMet": "✓ La contraseña cumple con todos los requisitos", - "passwordStrength": "Seguridad de la contraseña", - "passwordStrengthWeak": "Débil", - "passwordStrengthMedium": "Media", - "passwordStrengthStrong": "Fuerte", - "passwordRequirements": "Requisitos:", - "passwordRequirementLengthText": "8+ caracteres", - "passwordRequirementUppercaseText": "Letra mayúscula (A-Z)", - "passwordRequirementLowercaseText": "Letra minúscula (a-z)", - "passwordRequirementNumberText": "Número (0-9)", - "passwordRequirementSpecialText": "Caracter especial (!@#$%...)", - "passwordsDoNotMatch": "Las contraseñas no coinciden", - "otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter", - "otpEmailSent": "OTP enviado", - "otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico", - "otpEmailErrorAuthenticate": "Error al autenticar con el correo electrónico", - "pincodeErrorAuthenticate": "Error al autenticar con pincode", - "passwordErrorAuthenticate": "Error al autenticar con contraseña", - "poweredBy": "Desarrollado por", - "authenticationRequired": "Autenticación requerida", - "authenticationMethodChoose": "Elige tu método preferido para acceder a {name}", - "authenticationRequest": "Debes autenticarte para acceder a {name}", - "user": "Usuario", - "pincodeInput": "Código PIN de 6 dígitos", - "pincodeSubmit": "Iniciar sesión con PIN", - "passwordSubmit": "Iniciar sesión con contraseña", - "otpEmailDescription": "Se enviará un código único a este correo electrónico.", - "otpEmailSend": "Enviar código de una sola vez", - "otpEmail": "Contraseña de una sola vez (OTP)", - "otpEmailSubmit": "Enviar OTP", - "backToEmail": "Volver al Email", - "noSupportKey": "El servidor se está ejecutando sin una clave de soporte. ¡Considere apoyar el proyecto!", - "accessDenied": "Acceso denegado", - "accessDeniedDescription": "No tienes permiso para acceder a este recurso. Si esto es un error, por favor contacta con el administrador.", - "accessTokenError": "Error comprobando el token de acceso", - "accessGranted": "Acceso concedido", - "accessUrlInvalid": "URL de acceso inválida", - "accessGrantedDescription": "Se te ha concedido acceso a este recurso. Redirigiendo...", - "accessUrlInvalidDescription": "Esta URL de acceso compartido no es válida. Por favor, póngase en contacto con el propietario del recurso para una nueva URL.", - "tokenInvalid": "Token inválido", - "pincodeInvalid": "Código inválido", - "passwordErrorRequestReset": "Error al solicitar reinicio:", - "passwordErrorReset": "Error al restablecer la contraseña:", - "passwordResetSuccess": "¡Contraseña restablecida! Volver para iniciar sesión...", - "passwordReset": "Restablecer contraseña", - "passwordResetDescription": "Siga los pasos para restablecer su contraseña", - "passwordResetSent": "Enviaremos un código para restablecer la contraseña a esta dirección de correo electrónico.", - "passwordResetCode": "Código de restablecimiento", - "passwordResetCodeDescription": "Revisa tu correo electrónico para ver el código de restablecimiento.", - "passwordNew": "Nueva contraseña", - "passwordNewConfirm": "Confirmar nueva contraseña", - "pincodeAuth": "Código de autenticación", - "pincodeSubmit2": "Enviar código", - "passwordResetSubmit": "Reiniciar Solicitud", - "passwordBack": "Volver a la contraseña", - "loginBack": "Volver a iniciar sesión", - "signup": "Regístrate", - "loginStart": "Inicia sesión para empezar", - "idpOidcTokenValidating": "Validando token OIDC", - "idpOidcTokenResponse": "Validar respuesta de token OIDC", - "idpErrorOidcTokenValidating": "Error al validar token OIDC", - "idpConnectingTo": "Conectando a {name}", - "idpConnectingToDescription": "Validando tu identidad", - "idpConnectingToProcess": "Conectando...", - "idpConnectingToFinished": "Conectado", - "idpErrorConnectingTo": "Hubo un problema al conectar con {name}. Por favor, póngase en contacto con su administrador.", - "idpErrorNotFound": "IdP no encontrado", - "inviteInvalid": "Invitación inválida", - "inviteInvalidDescription": "El enlace de invitación no es válido.", - "inviteErrorWrongUser": "La invitación no es para este usuario", - "inviteErrorUserNotExists": "El usuario no existe. Por favor, cree una cuenta primero.", - "inviteErrorLoginRequired": "Debes estar conectado para aceptar una invitación", - "inviteErrorExpired": "La invitación puede haber caducado", - "inviteErrorRevoked": "La invitación podría haber sido revocada", - "inviteErrorTypo": "Puede haber un error en el enlace de invitación", - "pangolinSetup": "Configuración - Pangolin", - "orgNameRequired": "El nombre de la organización es obligatorio", - "orgIdRequired": "El ID de la organización es obligatorio", - "orgErrorCreate": "Se ha producido un error al crear el org", - "pageNotFound": "Página no encontrada", - "pageNotFoundDescription": "¡Vaya! La página que estás buscando no existe.", - "overview": "Resumen", - "home": "Inicio", - "accessControl": "Control de acceso", - "settings": "Ajustes", - "usersAll": "Todos los usuarios", - "license": "Licencia", - "pangolinDashboard": "Tablero - Pangolin", - "noResults": "No se han encontrado resultados.", - "terabytes": "TB {count}", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Etiquetas introducidas", - "tagsEnteredDescription": "Estas son las etiquetas que has introducido.", - "tagsWarnCannotBeLessThanZero": "maxTags y minTags no pueden ser menores que 0", - "tagsWarnNotAllowedAutocompleteOptions": "Etiqueta no permitida como opciones de autocompletado", - "tagsWarnInvalid": "Etiqueta no válida según validateTag", - "tagWarnTooShort": "La etiqueta {tagText} es demasiado corta", - "tagWarnTooLong": "La etiqueta {tagText} es demasiado larga", - "tagsWarnReachedMaxNumber": "Alcanzado el número máximo de etiquetas permitidas", - "tagWarnDuplicate": "Etiqueta {tagText} duplicada no añadida", - "supportKeyInvalid": "Clave inválida", - "supportKeyInvalidDescription": "Tu clave de seguidor no es válida.", - "supportKeyValid": "Clave válida", - "supportKeyValidDescription": "Su clave de seguidor ha sido validada. ¡Gracias por su apoyo!", - "supportKeyErrorValidationDescription": "Error al validar la clave de seguidor.", - "supportKey": "¡Apoya el Desarrollo y Adopte un Pangolin!", - "supportKeyDescription": "Compra una clave de seguidor para ayudarnos a seguir desarrollando Pangolin para la comunidad. Su contribución nos permite comprometer más tiempo para mantener y añadir nuevas características a la aplicación para todos. Nunca usaremos esto para las características de paywall. Esto está separado de cualquier Edición Comercial.", - "supportKeyPet": "También podrás adoptar y conocer a tu propio Pangolin mascota.", - "supportKeyPurchase": "Los pagos se procesan a través de GitHub. Después, puede recuperar su clave en", - "supportKeyPurchaseLink": "nuestro sitio web", - "supportKeyPurchase2": "y canjéelo aquí.", - "supportKeyLearnMore": "Más información.", - "supportKeyOptions": "Por favor, seleccione la opción que más le convenga.", - "supportKetOptionFull": "Asistente completo", - "forWholeServer": "Para todo el servidor", - "lifetimePurchase": "Compra de por vida", - "supporterStatus": "Estado del soporte", - "buy": "Comprar", - "supportKeyOptionLimited": "Apoyador limitado", - "forFiveUsers": "Para 5 o menos usuarios", - "supportKeyRedeem": "Canjear Clave de Apoyo", - "supportKeyHideSevenDays": "Ocultar durante 7 días", - "supportKeyEnter": "Introduzca Clave de Soporter", - "supportKeyEnterDescription": "Conoce a tu propia mascota Pangolin!", - "githubUsername": "Nombre de usuario de GitHub", - "supportKeyInput": "Clave de apoyo", - "supportKeyBuy": "Comprar Clave de Apoyo", - "logoutError": "Error al cerrar sesión", - "signingAs": "Conectado como", - "serverAdmin": "Admin Servidor", - "managedSelfhosted": "Autogestionado", - "otpEnable": "Activar doble factor", - "otpDisable": "Desactivar doble factor", - "logout": "Cerrar sesión", - "licenseTierProfessionalRequired": "Edición Profesional requerida", - "licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.", - "actionGetOrg": "Obtener organización", - "updateOrgUser": "Actualizar usuario Org", - "createOrgUser": "Crear usuario Org", - "actionUpdateOrg": "Actualizar organización", - "actionUpdateUser": "Actualizar usuario", - "actionGetUser": "Obtener usuario", - "actionGetOrgUser": "Obtener usuario de la organización", - "actionListOrgDomains": "Listar dominios de la organización", - "actionCreateSite": "Crear sitio", - "actionDeleteSite": "Eliminar sitio", - "actionGetSite": "Obtener sitio", - "actionListSites": "Listar sitios", - "actionApplyBlueprint": "Aplicar plano", - "setupToken": "Configuración de token", - "setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.", - "setupTokenRequired": "Se requiere el token de configuración", - "actionUpdateSite": "Actualizar sitio", - "actionListSiteRoles": "Lista de roles permitidos del sitio", - "actionCreateResource": "Crear Recurso", - "actionDeleteResource": "Eliminar Recurso", - "actionGetResource": "Obtener recursos", - "actionListResource": "Listar recursos", - "actionUpdateResource": "Actualizar Recurso", - "actionListResourceUsers": "Listar usuarios de recursos", - "actionSetResourceUsers": "Establecer usuarios de recursos", - "actionSetAllowedResourceRoles": "Establecer roles de recursos permitidos", - "actionListAllowedResourceRoles": "Lista de roles de recursos permitidos", - "actionSetResourcePassword": "Establecer contraseña de recurso", - "actionSetResourcePincode": "Establecer Pincode del recurso", - "actionSetResourceEmailWhitelist": "Establecer lista blanca de correo de recursos", - "actionGetResourceEmailWhitelist": "Obtener correo electrónico de recursos", - "actionCreateTarget": "Crear destino", - "actionDeleteTarget": "Eliminar destino", - "actionGetTarget": "Obtener objetivo", - "actionListTargets": "Lista de objetivos", - "actionUpdateTarget": "Actualizar destino", - "actionCreateRole": "Crear rol", - "actionDeleteRole": "Eliminar rol", - "actionGetRole": "Obtener rol", - "actionListRole": "Lista de roles", - "actionUpdateRole": "Actualizar rol", - "actionListAllowedRoleResources": "Lista de recursos de rol permitidos", - "actionInviteUser": "Invitar usuario", - "actionRemoveUser": "Eliminar usuario", - "actionListUsers": "Listar usuarios", - "actionAddUserRole": "Añadir rol de usuario", - "actionGenerateAccessToken": "Generar token de acceso", - "actionDeleteAccessToken": "Eliminar token de acceso", - "actionListAccessTokens": "Lista de Tokens de Acceso", - "actionCreateResourceRule": "Crear Regla de Recursos", - "actionDeleteResourceRule": "Eliminar Regla de Recurso", - "actionListResourceRules": "Lista de Reglas de Recursos", - "actionUpdateResourceRule": "Actualizar regla de recursos", - "actionListOrgs": "Listar organizaciones", - "actionCheckOrgId": "Comprobar ID", - "actionCreateOrg": "Crear organización", - "actionDeleteOrg": "Eliminar organización", - "actionListApiKeys": "Lista de claves API", - "actionListApiKeyActions": "Listar acciones clave API", - "actionSetApiKeyActions": "Establecer acciones de clave API permitidas", - "actionCreateApiKey": "Crear Clave API", - "actionDeleteApiKey": "Borrar Clave API", - "actionCreateIdp": "Crear IDP", - "actionUpdateIdp": "Actualizar IDP", - "actionDeleteIdp": "Eliminar IDP", - "actionListIdps": "Listar IDP", - "actionGetIdp": "Obtener IDP", - "actionCreateIdpOrg": "Crear política de IDP Org", - "actionDeleteIdpOrg": "Eliminar política de IDP Org", - "actionListIdpOrgs": "Listar Orgs IDP", - "actionUpdateIdpOrg": "Actualizar IDP Org", - "actionCreateClient": "Crear cliente", - "actionDeleteClient": "Eliminar cliente", - "actionUpdateClient": "Actualizar cliente", - "actionListClients": "Listar clientes", - "actionGetClient": "Obtener cliente", - "actionCreateSiteResource": "Crear Recurso del Sitio", - "actionDeleteSiteResource": "Eliminar recurso del sitio", - "actionGetSiteResource": "Obtener recurso del sitio", - "actionListSiteResources": "Listar recursos del sitio", - "actionUpdateSiteResource": "Actualizar recurso del sitio", - "actionListInvitations": "Listar invitaciones", - "noneSelected": "Ninguno seleccionado", - "orgNotFound2": "No se encontraron organizaciones.", - "searchProgress": "Buscar...", - "create": "Crear", - "orgs": "Organizaciones", - "loginError": "Se ha producido un error al iniciar sesión", - "passwordForgot": "¿Olvidaste tu contraseña?", - "otpAuth": "Autenticación de dos factores", - "otpAuthDescription": "Introduzca el código de su aplicación de autenticación o uno de sus códigos de copia de seguridad de un solo uso.", - "otpAuthSubmit": "Enviar código", - "idpContinue": "O continuar con", - "otpAuthBack": "Volver a iniciar sesión", - "navbar": "Menú de navegación", - "navbarDescription": "Menú de navegación principal para la aplicación", - "navbarDocsLink": "Documentación", - "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", - "otpSetupCheckCodeRetry": "Código no válido. Vuelve a intentarlo.", - "otpSetup": "Habilitar autenticación de doble factor", - "otpSetupDescription": "Asegure su cuenta con una capa extra de protección", - "otpSetupScanQr": "Escanea este código QR con tu aplicación de autenticación o introduce la clave secreta manualmente:", - "otpSetupSecretCode": "Código de autenticación", - "otpSetupSuccess": "Autenticación de dos factores habilitada", - "otpSetupSuccessStoreBackupCodes": "Tu cuenta ahora es más segura. No olvides guardar tus códigos de respaldo.", - "otpErrorDisable": "No se puede desactivar 2FA", - "otpErrorDisableDescription": "Se ha producido un error al desactivar 2FA", - "otpRemove": "Desactivar autenticación de doble factor", - "otpRemoveDescription": "Desactivar autenticación de doble factor para su cuenta", - "otpRemoveSuccess": "Autenticación de dos factores desactivada", - "otpRemoveSuccessMessage": "La autenticación de doble factor ha sido deshabilitada para su cuenta. Puede activarla de nuevo en cualquier momento.", - "otpRemoveSubmit": "Desactivar 2FA", - "paginator": "Página {current} de {last}", - "paginatorToFirst": "Ir a la primera página", - "paginatorToPrevious": "Ir a la página anterior", - "paginatorToNext": "Ir a la página siguiente", - "paginatorToLast": "Ir a la última página", - "copyText": "Copiar texto", - "copyTextFailed": "Error al copiar texto: ", - "copyTextClipboard": "Copiar al portapapeles", - "inviteErrorInvalidConfirmation": "Confirmación no válida", - "passwordRequired": "Se requiere contraseña", - "allowAll": "Permitir todo", - "permissionsAllowAll": "Permitir todos los permisos", - "githubUsernameRequired": "Se requiere el nombre de usuario de GitHub", - "supportKeyRequired": "Clave de apoyo es requerida", - "passwordRequirementsChars": "La contraseña debe tener al menos 8 caracteres", - "language": "Idioma", - "verificationCodeRequired": "El código es requerido", - "userErrorNoUpdate": "Ningún usuario para actualizar", - "siteErrorNoUpdate": "No hay sitio para actualizar", - "resourceErrorNoUpdate": "Ningún recurso para actualizar", - "authErrorNoUpdate": "No hay información de autenticación para actualizar", - "orgErrorNoUpdate": "No hay org para actualizar", - "orgErrorNoProvided": "No hay org proporcionado", - "apiKeysErrorNoUpdate": "Ninguna clave API para actualizar", - "sidebarOverview": "Resumen", - "sidebarHome": "Inicio", - "sidebarSites": "Sitios", - "sidebarResources": "Recursos", - "sidebarAccessControl": "Control de acceso", - "sidebarUsers": "Usuarios", - "sidebarInvitations": "Invitaciones", - "sidebarRoles": "Roles", - "sidebarShareableLinks": "Enlaces compartibles", - "sidebarApiKeys": "Claves API", - "sidebarSettings": "Ajustes", - "sidebarAllUsers": "Todos los usuarios", - "sidebarIdentityProviders": "Proveedores de identidad", - "sidebarLicense": "Licencia", - "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.", - "enableDockerSocketLink": "Saber más", - "viewDockerContainers": "Ver contenedores Docker", - "containersIn": "Contenedores en {siteName}", - "selectContainerDescription": "Seleccione cualquier contenedor para usar como nombre de host para este objetivo. Haga clic en un puerto para usar un puerto.", - "containerName": "Nombre", - "containerImage": "Imagen", - "containerState": "Estado", - "containerNetworks": "Redes", - "containerHostnameIp": "Nombre del host/IP", - "containerLabels": "Etiquetas", - "containerLabelsCount": "{count, plural, one {# etiqueta} other {# etiquetas}}", - "containerLabelsTitle": "Etiquetas de contenedor", - "containerLabelEmpty": "", - "containerPorts": "Puertos", - "containerPortsMore": "+{count} más", - "containerActions": "Acciones", - "select": "Seleccionar", - "noContainersMatchingFilters": "No se encontraron contenedores que coincidan con los filtros actuales.", - "showContainersWithoutPorts": "Mostrar contenedores sin puertos", - "showStoppedContainers": "Mostrar contenedores parados", - "noContainersFound": "No se han encontrado contenedores. Asegúrate de que los contenedores Docker se estén ejecutando.", - "searchContainersPlaceholder": "Buscar a través de contenedores {count}...", - "searchResultsCount": "{count, plural, one {# resultado} other {# resultados}}", - "filters": "Filtros", - "filterOptions": "Opciones de filtro", - "filterPorts": "Puertos", - "filterStopped": "Detenido", - "clearAllFilters": "Borrar todos los filtros", - "columns": "Columnas", - "toggleColumns": "Cambiar Columnas", - "refreshContainersList": "Actualizar lista de contenedores", - "searching": "Buscando...", - "noContainersFoundMatching": "No se han encontrado contenedores que coincidan con \"{filter}\".", - "light": "claro", - "dark": "oscuro", - "system": "sistema", - "theme": "Tema", - "subnetRequired": "Se requiere subred", - "initialSetupTitle": "Configuración inicial del servidor", - "initialSetupDescription": "Cree la cuenta de administrador del servidor inicial. Solo puede existir un administrador del servidor. Siempre puede cambiar estas credenciales más tarde.", - "createAdminAccount": "Crear cuenta de administrador", - "setupErrorCreateAdmin": "Se produjo un error al crear la cuenta de administrador del servidor.", - "certificateStatus": "Estado del certificado", - "loading": "Cargando", - "restart": "Reiniciar", - "domains": "Dominios", - "domainsDescription": "Administrar dominios de tu organización", - "domainsSearch": "Buscar dominios...", - "domainAdd": "Agregar dominio", - "domainAddDescription": "Registrar un nuevo dominio con tu organización", - "domainCreate": "Crear dominio", - "domainCreatedDescription": "Dominio creado con éxito", - "domainDeletedDescription": "Dominio eliminado exitosamente", - "domainQuestionRemove": "¿Está seguro de que desea eliminar el dominio {domain} de su cuenta?", - "domainMessageRemove": "Una vez eliminado, el dominio ya no estará asociado con su cuenta.", - "domainMessageConfirm": "Para confirmar, por favor escriba el nombre del dominio abajo.", - "domainConfirmDelete": "Confirmar eliminación del dominio", - "domainDelete": "Eliminar dominio", - "domain": "Dominio", - "selectDomainTypeNsName": "Delegación de dominio (NS)", - "selectDomainTypeNsDescription": "Este dominio y todos sus subdominios. Usa esto cuando quieras controlar una zona de dominio completa.", - "selectDomainTypeCnameName": "Dominio único (CNAME)", - "selectDomainTypeCnameDescription": "Solo este dominio específico. Úsalo para subdominios individuales o entradas específicas de dominio.", - "selectDomainTypeWildcardName": "Dominio comodín", - "selectDomainTypeWildcardDescription": "Este dominio y sus subdominios.", - "domainDelegation": "Dominio único", - "selectType": "Selecciona un tipo", - "actions": "Acciones", - "refresh": "Actualizar", - "refreshError": "Error al actualizar datos", - "verified": "Verificado", - "pending": "Pendiente", - "sidebarBilling": "Facturación", - "billing": "Facturación", - "orgBillingDescription": "Gestiona tu información de facturación y suscripciones", - "github": "GitHub", - "pangolinHosted": "Pangolin Alojado", - "fossorial": "Fossorial", - "completeAccountSetup": "Completar configuración de cuenta", - "completeAccountSetupDescription": "Establece tu contraseña para comenzar", - "accountSetupSent": "Enviaremos un código de configuración de cuenta a esta dirección de correo electrónico.", - "accountSetupCode": "Código de configuración", - "accountSetupCodeDescription": "Revisa tu correo para el código de configuración.", - "passwordCreate": "Crear contraseña", - "passwordCreateConfirm": "Confirmar contraseña", - "accountSetupSubmit": "Enviar código de configuración", - "completeSetup": "Completar configuración", - "accountSetupSuccess": "¡Configuración de cuenta completada! ¡Bienvenido a Pangolin!", - "documentation": "Documentación", - "saveAllSettings": "Guardar todos los ajustes", - "settingsUpdated": "Ajustes actualizados", - "settingsUpdatedDescription": "Todos los ajustes han sido actualizados exitosamente", - "settingsErrorUpdate": "Error al actualizar ajustes", - "settingsErrorUpdateDescription": "Ocurrió un error al actualizar ajustes", - "sidebarCollapse": "Colapsar", - "sidebarExpand": "Expandir", - "newtUpdateAvailable": "Nueva actualización disponible", - "newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.", - "domainPickerEnterDomain": "Dominio", - "domainPickerPlaceholder": "miapp.ejemplo.com", - "domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.", - "domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles", - "domainPickerTabAll": "Todo", - "domainPickerTabOrganization": "Organización", - "domainPickerTabProvided": "Proporcionado", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Comprobando disponibilidad...", - "domainPickerNoMatchingDomains": "No se encontraron dominios que coincidan. Intente con un dominio diferente o verifique la configuración de dominios de su organización.", - "domainPickerOrganizationDomains": "Dominios de la organización", - "domainPickerProvidedDomains": "Dominios proporcionados", - "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", - "createNewOrgDescription": "Crear una nueva organización", - "organization": "Organización", - "port": "Puerto", - "securityKeyManage": "Gestionar llaves de seguridad", - "securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña", - "securityKeyRegister": "Registrar nueva llave de seguridad", - "securityKeyList": "Tus llaves de seguridad", - "securityKeyNone": "No hay llaves de seguridad registradas", - "securityKeyNameRequired": "El nombre es requerido", - "securityKeyRemove": "Eliminar", - "securityKeyLastUsed": "Último uso: {date}", - "securityKeyNameLabel": "Nombre", - "securityKeyRegisterSuccess": "Llave de seguridad registrada exitosamente", - "securityKeyRegisterError": "Error al registrar la llave de seguridad", - "securityKeyRemoveSuccess": "Llave de seguridad eliminada exitosamente", - "securityKeyRemoveError": "Error al eliminar la llave de seguridad", - "securityKeyLoadError": "Error al cargar las llaves de seguridad", - "securityKeyLogin": "Continuar con clave de seguridad", - "securityKeyAuthError": "Error al autenticar con llave de seguridad", - "securityKeyRecommendation": "Considere registrar otra llave de seguridad en un dispositivo diferente para asegurarse de no quedar bloqueado de su cuenta.", - "registering": "Registrando...", - "securityKeyPrompt": "Por favor, verifica tu identidad usando tu llave de seguridad. Asegúrate de que tu llave de seguridad esté conectada y lista.", - "securityKeyBrowserNotSupported": "Tu navegador no admite llaves de seguridad. Por favor, usa un navegador moderno como Chrome, Firefox o Safari.", - "securityKeyPermissionDenied": "Por favor, permite el acceso a tu llave de seguridad para continuar iniciando sesión.", - "securityKeyRemovedTooQuickly": "Por favor, mantén tu llave de seguridad conectada hasta que el proceso de inicio de sesión se complete.", - "securityKeyNotSupported": "Tu llave de seguridad puede no ser compatible. Por favor, prueba con una llave de seguridad diferente.", - "securityKeyUnknownError": "Hubo un problema al usar tu llave de seguridad. Por favor, inténtalo de nuevo.", - "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.", - "securityKeyAdd": "Agregar llave de seguridad", - "securityKeyRegisterTitle": "Registrar nueva llave de seguridad", - "securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla", - "securityKeyTwoFactorRequired": "Se requiere autenticación de dos factores", - "securityKeyTwoFactorDescription": "Por favor, ingresa tu código de autenticación de dos factores para registrar la llave de seguridad", - "securityKeyTwoFactorRemoveDescription": "Por favor, ingresa tu código de autenticación de dos factores para eliminar la llave de seguridad", - "securityKeyTwoFactorCode": "Código de autenticación de dos factores", - "securityKeyRemoveTitle": "Eliminar llave de seguridad", - "securityKeyRemoveDescription": "Ingresa tu contraseña para eliminar la llave de seguridad \"{name}\"", - "securityKeyNoKeysRegistered": "No hay llaves de seguridad registradas", - "securityKeyNoKeysDescription": "Agrega una llave de seguridad para mejorar la seguridad de tu cuenta", - "createDomainRequired": "Se requiere dominio", - "createDomainAddDnsRecords": "Agregar registros DNS", - "createDomainAddDnsRecordsDescription": "Agrega los siguientes registros DNS a tu proveedor de dominios para completar la configuración.", - "createDomainNsRecords": "Registros NS", - "createDomainRecord": "Registro", - "createDomainType": "Tipo:", - "createDomainName": "Nombre:", - "createDomainValue": "Valor:", - "createDomainCnameRecords": "Registros CNAME", - "createDomainARecords": "Registros A", - "createDomainRecordNumber": "Registro {number}", - "createDomainTxtRecords": "Registros TXT", - "createDomainSaveTheseRecords": "Guardar estos registros", - "createDomainSaveTheseRecordsDescription": "Asegúrate de guardar estos registros DNS ya que no los verás de nuevo.", - "createDomainDnsPropagation": "Propagación DNS", - "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", - "and": "y", - "privacyPolicy": "política de privacidad" - }, - "siteRequired": "El sitio es requerido.", - "olmTunnel": "Túnel Olm", - "olmTunnelDescription": "Usar Olm para la conectividad del cliente", - "errorCreatingClient": "Error al crear el cliente", - "clientDefaultsNotFound": "Configuración predeterminada del cliente no encontrada", - "createClient": "Crear cliente", - "createClientDescription": "Crear un cliente nuevo para conectar a sus sitios", - "seeAllClients": "Ver todos los clientes", - "clientInformation": "Información del cliente", - "clientNamePlaceholder": "Nombre del cliente", - "address": "Dirección", - "subnetPlaceholder": "Subred", - "addressDescription": "La dirección que este cliente utilizará para la conectividad", - "selectSites": "Seleccionar sitios", - "sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados", - "clientInstallOlm": "Instalar Olm", - "clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema", - "clientOlmCredentials": "Credenciales Olm", - "clientOlmCredentialsDescription": "Así es como Olm se autentificará con el servidor", - "olmEndpoint": "Punto final Olm", - "olmId": "ID de Olm", - "olmSecretKey": "Clave secreta de Olm", - "clientCredentialsSave": "Guarda tus credenciales", - "clientCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.", - "generalSettingsDescription": "Configura la configuración general para este cliente", - "clientUpdated": "Cliente actualizado", - "clientUpdatedDescription": "El cliente ha sido actualizado.", - "clientUpdateFailed": "Error al actualizar el cliente", - "clientUpdateError": "Se ha producido un error al actualizar el cliente.", - "sitesFetchFailed": "Error al obtener los sitios", - "sitesFetchError": "Se ha producido un error al recuperar los sitios.", - "olmErrorFetchReleases": "Se ha producido un error al recuperar las versiones de Olm.", - "olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.", - "remoteSubnets": "Subredes remotas", - "enterCidrRange": "Ingresa el rango CIDR", - "remoteSubnetsDescription": "Agregue rangos CIDR que se puedan acceder desde este sitio de forma remota usando clientes. Utilice el formato como 10.0.0.0/24. Esto SOLO se aplica a la conectividad del cliente VPN.", - "resourceEnableProxy": "Habilitar proxy público", - "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", - "externalProxyEnabled": "Proxy externo habilitado", - "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", - "domainPickerBaseDomainLabel": "Dominio base", - "domainPickerSearchDomains": "Buscar dominios...", - "domainPickerNoDomainsFound": "No se encontraron dominios", - "domainPickerLoadingDomains": "Cargando dominios...", - "domainPickerSelectBaseDomain": "Seleccionar dominio base...", - "domainPickerNotAvailableForCname": "No disponible para dominios CNAME", - "domainPickerEnterSubdomainOrLeaveBlank": "Ingrese subdominio o deje en blanco para usar dominio base.", - "domainPickerEnterSubdomainToSearch": "Ingrese un subdominio para buscar y seleccionar entre dominios gratuitos disponibles.", - "domainPickerFreeDomains": "Dominios gratuitos", - "domainPickerSearchForAvailableDomains": "Buscar dominios disponibles", - "domainPickerNotWorkSelfHosted": "Nota: Los dominios gratuitos proporcionados no están disponibles para instancias autogestionadas por ahora.", - "resourceDomain": "Dominio", - "resourceEditDomain": "Editar dominio", - "siteName": "Nombre del sitio", - "proxyPort": "Puerto", - "resourcesTableProxyResources": "Recursos de proxy", - "resourcesTableClientResources": "Recursos del cliente", - "resourcesTableNoProxyResourcesFound": "No se encontraron recursos de proxy.", - "resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.", - "resourcesTableDestination": "Destino", - "resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con", - "resourcesTableClients": "Clientes", - "resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.", - "editInternalResourceDialogEditClientResource": "Editar recurso del cliente", - "editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.", - "editInternalResourceDialogResourceProperties": "Propiedades del recurso", - "editInternalResourceDialogName": "Nombre", - "editInternalResourceDialogProtocol": "Protocolo", - "editInternalResourceDialogSitePort": "Puerto del sitio", - "editInternalResourceDialogTargetConfiguration": "Configuración de objetivos", - "editInternalResourceDialogCancel": "Cancelar", - "editInternalResourceDialogSaveResource": "Guardar recurso", - "editInternalResourceDialogSuccess": "Éxito", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno actualizado con éxito", - "editInternalResourceDialogError": "Error", - "editInternalResourceDialogFailedToUpdateInternalResource": "Error al actualizar el recurso interno", - "editInternalResourceDialogNameRequired": "El nombre es requerido", - "editInternalResourceDialogNameMaxLength": "El nombre no debe tener más de 255 caracteres", - "editInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1", - "editInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido", - "editInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1", - "editInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536", - "createInternalResourceDialogNoSitesAvailable": "No hay sitios disponibles", - "createInternalResourceDialogNoSitesAvailableDescription": "Necesita tener al menos un sitio de Newt con una subred configurada para crear recursos internos.", - "createInternalResourceDialogClose": "Cerrar", - "createInternalResourceDialogCreateClientResource": "Crear recurso del cliente", - "createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que será accesible para los clientes conectados al sitio seleccionado.", - "createInternalResourceDialogResourceProperties": "Propiedades del recurso", - "createInternalResourceDialogName": "Nombre", - "createInternalResourceDialogSite": "Sitio", - "createInternalResourceDialogSelectSite": "Seleccionar sitio...", - "createInternalResourceDialogSearchSites": "Buscar sitios...", - "createInternalResourceDialogNoSitesFound": "Sitios no encontrados.", - "createInternalResourceDialogProtocol": "Protocolo", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Puerto del sitio", - "createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.", - "createInternalResourceDialogTargetConfiguration": "Configuración de objetivos", - "createInternalResourceDialogDestinationIPDescription": "La dirección IP o nombre de host del recurso en la red del sitio.", - "createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.", - "createInternalResourceDialogCancel": "Cancelar", - "createInternalResourceDialogCreateResource": "Crear recurso", - "createInternalResourceDialogSuccess": "Éxito", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno creado con éxito", - "createInternalResourceDialogError": "Error", - "createInternalResourceDialogFailedToCreateInternalResource": "Error al crear recurso interno", - "createInternalResourceDialogNameRequired": "El nombre es requerido", - "createInternalResourceDialogNameMaxLength": "El nombre debe ser menor de 255 caracteres", - "createInternalResourceDialogPleaseSelectSite": "Por favor seleccione un sitio", - "createInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1", - "createInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido", - "createInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1", - "createInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536", - "siteConfiguration": "Configuración", - "siteAcceptClientConnections": "Aceptar conexiones de clientes", - "siteAcceptClientConnectionsDescription": "Permitir que otros dispositivos se conecten a través de esta instancia Newt como una puerta de enlace utilizando clientes.", - "siteAddress": "Dirección del sitio", - "siteAddressDescription": "Especifique la dirección IP del host que los clientes deben usar para conectarse. Esta es la dirección interna del sitio en la red de Pangolín para que los clientes dirijan. Debe estar dentro de la subred de la organización.", - "autoLoginExternalIdp": "Inicio de sesión automático con IDP externo", - "autoLoginExternalIdpDescription": "Redirigir inmediatamente al usuario al IDP externo para autenticación.", - "selectIdp": "Seleccionar IDP", - "selectIdpPlaceholder": "Elegir un IDP...", - "selectIdpRequired": "Por favor seleccione un IDP cuando el inicio de sesión automático esté habilitado.", - "autoLoginTitle": "Redirigiendo", - "autoLoginDescription": "Te estamos redirigiendo al proveedor de identidad externo para autenticación.", - "autoLoginProcessing": "Preparando autenticación...", - "autoLoginRedirecting": "Redirigiendo al inicio de sesión...", - "autoLoginError": "Error de inicio de sesión automático", - "autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.", - "autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.", - "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", - "introTitle": "Pangolin autogestionado", - "introDescription": "es una opción de despliegue diseñada para personas que quieren simplicidad y fiabilidad extra mientras mantienen sus datos privados y autoalojados.", - "introDetail": "Con esta opción, todavía ejecuta su propio nodo Pangolin, sus túneles, terminación SSL y tráfico permanecen en su servidor. La diferencia es que la gestión y el control se gestionan a través de nuestro panel de control en la nube, que desbloquea una serie de ventajas:", - "benefitSimplerOperations": { - "title": "Operaciones simples", - "description": "No necesitas ejecutar tu propio servidor de correo o configurar alertas complejas. Recibirás cheques de salud y alertas de tiempo de inactividad." - }, - "benefitAutomaticUpdates": { - "title": "Actualizaciones automáticas", - "description": "El tablero de la nube evolucionará rápidamente, por lo que obtendrá nuevas características y correcciones de errores sin tener que extraer manualmente nuevos contenedores cada vez." - }, - "benefitLessMaintenance": { - "title": "Menos mantenimiento", - "description": "No hay migraciones de base de datos, copias de seguridad o infraestructura extra para administrar. Lo manejamos en la nube." - }, - "benefitCloudFailover": { - "title": "Fallo en la nube", - "description": "Si tu nodo cae, tus túneles pueden fallar temporalmente a nuestros puntos de presencia en la nube hasta que lo vuelvas a conectar." - }, - "benefitHighAvailability": { - "title": "Alta disponibilidad (PoPs)", - "description": "También puede adjuntar múltiples nodos a su cuenta para redundancia y mejor rendimiento." - }, - "benefitFutureEnhancements": { - "title": "Mejoras futuras", - "description": "Estamos planeando añadir más herramientas analíticas, alertas y de administración para hacer su despliegue aún más robusto." - }, - "docsAlert": { - "text": "Aprenda más acerca de la opción de autoalojamiento administrado en nuestra", - "documentation": "documentación" - }, - "convertButton": "Convierte este nodo a autoalojado administrado" - }, - "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", - "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", - "domainPickerUnverified": "Sin verificar", - "domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.", - "domainPickerError": "Error", - "domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización", - "domainPickerErrorCheckAvailability": "No se pudo comprobar la disponibilidad del dominio", - "domainPickerInvalidSubdomain": "Subdominio inválido", - "domainPickerInvalidSubdomainRemoved": "La entrada \"{sub}\" fue eliminada porque no es válida.", - "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", - "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 deleted file mode 100644 index 91d69844..00000000 --- a/messages/fr-FR.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Créez votre organisation, votre site et vos ressources", - "setupNewOrg": "Nouvelle organisation", - "setupCreateOrg": "Créer une organisation", - "setupCreateResources": "Créer des ressources", - "setupOrgName": "Nom de l'organisation", - "orgDisplayName": "Ceci est le nom d'affichage de votre organisation.", - "orgId": "ID de l'organisation", - "setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.", - "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 à Pangolin", - "welcomeTo": "Bienvenue chez", - "componentsCreateOrg": "Créer une organisation", - "componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.", - "componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", - "dismiss": "Refuser", - "componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", - "componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!", - "inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.", - "inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.", - "inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.", - "inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.", - "inviteCreateUser": "Veuillez d'abord créer un compte.", - "goHome": "Retour à la maison", - "inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent", - "createAnAccount": "Créer un compte", - "inviteNotAccepted": "Invitation non acceptée", - "authCreateAccount": "Créez un compte pour commencer", - "authNoAccount": "Vous n'avez pas de compte ?", - "email": "Courriel", - "password": "Mot de passe", - "confirmPassword": "Confirmer le mot de passe", - "createAccount": "Créer un compte", - "viewSettings": "Afficher les paramètres", - "delete": "Supprimez", - "name": "Nom", - "online": "En ligne", - "offline": "Hors ligne", - "site": "Site", - "dataIn": "Données dans", - "dataOut": "Données épuisées", - "connectionType": "Type de connexion", - "tunnelType": "Type de tunnel", - "local": "Locale", - "edit": "Editer", - "siteConfirmDelete": "Confirmer la suppression du site", - "siteDelete": "Supprimer le site", - "siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.", - "siteMessageConfirm": "Pour confirmer, veuillez saisir le nom du site ci-dessous.", - "siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer le site {selectedSite} de l'organisation ?", - "siteManageSites": "Gérer les sites", - "siteDescription": "Autoriser la connectivité à votre réseau via des tunnels sécurisés", - "siteCreate": "Créer un site", - "siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau site", - "siteCreateDescription": "Créez un nouveau site pour commencer à connecter vos ressources", - "close": "Fermer", - "siteErrorCreate": "Erreur lors de la création du site", - "siteErrorCreateKeyPair": "Paire de clés ou site par défaut introuvable", - "siteErrorCreateDefaults": "Les valeurs par défaut du site sont introuvables", - "method": "Méthode", - "siteMethodDescription": "C'est ainsi que vous exposerez les connexions.", - "siteLearnNewt": "Apprenez à installer Newt sur votre système", - "siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.", - "siteLoadWGConfig": "Chargement de la configuration WireGuard...", - "siteDocker": "Développer les détails du déploiement Docker", - "toggle": "Activer/désactiver", - "dockerCompose": "Composition Docker", - "dockerRun": "Exécution Docker", - "siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus", - "siteConfirmCopy": "J'ai copié la configuration", - "searchSitesProgress": "Rechercher des sites...", - "siteAdd": "Ajouter un site", - "siteInstallNewt": "Installer Newt", - "siteInstallNewtDescription": "Faites fonctionner Newt sur votre système", - "WgConfiguration": "Configuration WireGuard", - "WgConfigurationDescription": "Utilisez la configuration suivante pour vous connecter à votre réseau", - "operatingSystem": "Système d'exploitation", - "commands": "Commandes", - "recommended": "Recommandé", - "siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.", - "siteRunsInDocker": "Exécute dans Docker", - "siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows", - "siteErrorDelete": "Erreur lors de la suppression du site", - "siteErrorUpdate": "Impossible de mettre à jour le site", - "siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.", - "siteUpdated": "Site mis à jour", - "siteUpdatedDescription": "Le site a été mis à jour.", - "siteGeneralDescription": "Configurer les paramètres généraux de ce site", - "siteSettingDescription": "Configurer les paramètres de votre site", - "siteSetting": "Réglages {siteName}", - "siteNewtTunnel": "Tunnel Newt (Recommandé)", - "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.", - "siteWg": "WireGuard basique", - "siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", - "siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", - "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", - "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", - "siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur", - "siteCredentialsSave": "Enregistrez vos identifiants", - "siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.", - "siteInfo": "Informations sur le site", - "status": "Statut", - "shareTitle": "Gérer les liens de partage", - "shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent à vos ressources", - "shareSearch": "Rechercher des liens de partage...", - "shareCreate": "Créer un lien de partage", - "shareErrorDelete": "Impossible de supprimer le lien", - "shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien", - "shareDeleted": "Lien supprimé", - "shareDeletedDescription": "Le lien a été supprimé", - "shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.", - "accessToken": "Jeton d'accès", - "usageExamples": "Exemples d'utilisation", - "tokenId": "ID du jeton", - "requestHeades": "En-têtes de la requête", - "queryParameter": "Paramètre de requête", - "importantNote": "Note importante", - "shareImportantDescription": "Pour des raisons de sécurité, l'utilisation des en-têtes est recommandée par rapport aux paramètres de la requête, dans la mesure du possible, car les paramètres de requête peuvent être enregistrés dans les journaux du serveur ou dans l'historique du navigateur.", - "token": "Jeton", - "shareTokenSecurety": "Gardez votre jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.", - "shareErrorFetchResource": "Impossible de récupérer les ressources", - "shareErrorFetchResourceDescription": "Une erreur est survenue lors de la récupération des ressources", - "shareErrorCreate": "Impossible de créer le lien de partage", - "shareErrorCreateDescription": "Une erreur s'est produite lors de la création du lien de partage", - "shareCreateDescription": "N'importe qui avec ce lien peut accéder à la ressource", - "shareTitleOptional": "Titre (facultatif)", - "expireIn": "Expire dans", - "neverExpire": "N'expire jamais", - "shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.", - "shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.", - "shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.", - "shareTokenUsage": "Voir Utilisation du jeton d'accès", - "createLink": "Créer un lien", - "resourcesNotFound": "Aucune ressource trouvée", - "resourceSearch": "Rechercher des ressources", - "openMenu": "Ouvrir le menu", - "resource": "Ressource", - "title": "Titre de la page", - "created": "Créé", - "expires": "Expire", - "never": "Jamais", - "shareErrorSelectResource": "Veuillez sélectionner une ressource", - "resourceTitle": "Gérer les ressources", - "resourceDescription": "Créez des proxy sécurisés pour vos applications privées", - "resourcesSearch": "Rechercher des ressources...", - "resourceAdd": "Ajouter une ressource", - "resourceErrorDelte": "Erreur de suppression de la ressource", - "authentication": "Authentification", - "protected": "Protégé", - "notProtected": "Non Protégé", - "resourceMessageRemove": "Une fois supprimée, la ressource ne sera plus accessible. Toutes les cibles associées à la ressource seront également supprimées.", - "resourceMessageConfirm": "Pour confirmer, veuillez saisir le nom de la ressource ci-dessous.", - "resourceQuestionRemove": "Êtes-vous sûr de vouloir supprimer la ressource {selectedResource} de l'organisation ?", - "resourceHTTP": "Ressource HTTPS", - "resourceHTTPDescription": "Requêtes de proxy à votre application via HTTPS en utilisant un sous-domaine ou un domaine de base.", - "resourceRaw": "Ressource TCP/UDP brute", - "resourceRawDescription": "Demandes de proxy à votre application via TCP/UDP en utilisant un numéro de port.", - "resourceCreate": "Créer une ressource", - "resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource", - "resourceSeeAll": "Voir toutes les ressources", - "resourceInfo": "Informations sur la ressource", - "resourceNameDescription": "Ceci est le nom d'affichage de la ressource.", - "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", - "resourceHTTPSSettings": "Paramètres HTTPS", - "resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS", - "domainType": "Type de domaine", - "subdomain": "Sous-domaine", - "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", - "protocol": "Protocole", - "protocolSelect": "Sélectionner un protocole", - "resourcePortNumber": "Numéro de port", - "resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.", - "cancel": "Abandonner", - "resourceConfig": "Snippets de configuration", - "resourceConfigDescription": "Copiez et collez ces modules de configuration pour configurer votre ressource TCP/UDP", - "resourceAddEntrypoints": "Traefik: Ajouter des points d'entrée", - "resourceExposePorts": "Gerbil: Exposer des ports dans Docker Compose", - "resourceLearnRaw": "Apprenez à configurer les ressources TCP/UDP", - "resourceBack": "Retour aux ressources", - "resourceGoTo": "Aller à la ressource", - "resourceDelete": "Supprimer la ressource", - "resourceDeleteConfirm": "Confirmer la suppression de la ressource", - "visibility": "Visibilité", - "enabled": "Activé", - "disabled": "Désactivé", - "general": "Généraux", - "generalSettings": "Paramètres généraux", - "proxy": "Proxy", - "internal": "Interne", - "rules": "Règles", - "resourceSettingDescription": "Configurer les paramètres de votre ressource", - "resourceSetting": "Réglages {resourceName}", - "alwaysAllow": "Toujours autoriser", - "alwaysDeny": "Toujours refuser", - "passToAuth": "Paser à l'authentification", - "orgSettingsDescription": "Configurer les paramètres généraux de votre organisation", - "orgGeneralSettings": "Paramètres de l'organisation", - "orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation", - "saveGeneralSettings": "Enregistrer les paramètres généraux", - "saveSettings": "Enregistrer les paramètres", - "orgDangerZone": "Zone de danger", - "orgDangerZoneDescription": "Une fois que vous supprimez cette organisation, il n'y a pas de retour en arrière. Soyez certain.", - "orgDelete": "Supprimer l'organisation", - "orgDeleteConfirm": "Confirmer la suppression de l'organisation", - "orgMessageRemove": "Cette action est irréversible et supprimera toutes les données associées.", - "orgMessageConfirm": "Pour confirmer, veuillez saisir le nom de l'organisation ci-dessous.", - "orgQuestionRemove": "Êtes-vous sûr de vouloir supprimer l'organisation {selectedOrg}?", - "orgUpdated": "Organisation mise à jour", - "orgUpdatedDescription": "L'organisation a été mise à jour.", - "orgErrorUpdate": "Échec de la mise à jour de l'organisation", - "orgErrorUpdateMessage": "Une erreur s'est produite lors de la mise à jour de l'organisation.", - "orgErrorFetch": "Impossible de récupérer les organisations", - "orgErrorFetchMessage": "Une erreur s'est produite lors de la liste de vos organisations", - "orgErrorDelete": "Échec de la suppression de l'organisation", - "orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.", - "orgDeleted": "Organisation supprimée", - "orgDeletedMessage": "L'organisation et ses données ont été supprimées.", - "orgMissing": "ID d'organisation manquant", - "orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.", - "accessUsersManage": "Gérer les utilisateurs", - "accessUsersDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation", - "accessUsersSearch": "Rechercher des utilisateurs...", - "accessUserCreate": "Créer un utilisateur", - "accessUserRemove": "Supprimer l'utilisateur", - "username": "Nom d'utilisateur", - "identityProvider": "Fournisseur d'identité", - "role": "Rôle", - "nameRequired": "Le nom est requis", - "accessRolesManage": "Gérer les rôles", - "accessRolesDescription": "Configurer les rôles pour gérer l'accès à votre organisation", - "accessRolesSearch": "Rechercher des rôles...", - "accessRolesAdd": "Ajouter un rôle", - "accessRoleDelete": "Supprimer le rôle", - "description": "Libellé", - "inviteTitle": "Invitations ouvertes", - "inviteDescription": "Gérer vos invitations à d'autres utilisateurs", - "inviteSearch": "Rechercher des invitations...", - "minutes": "Minutes", - "hours": "Heures", - "days": "Jours", - "weeks": "Semaines", - "months": "Mois", - "years": "Années", - "day": "{count, plural, one {# jour} other {# jours}}", - "apiKeysTitle": "Informations sur la clé API", - "apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé API.", - "apiKeysErrorCreate": "Erreur lors de la création de la clé API", - "apiKeysErrorSetPermission": "Erreur lors de la définition des permissions", - "apiKeysCreate": "Générer une clé API", - "apiKeysCreateDescription": "Générer une nouvelle clé API pour votre organisation", - "apiKeysGeneralSettings": "Permissions", - "apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire", - "apiKeysList": "Votre clé API", - "apiKeysSave": "Enregistrer votre clé API", - "apiKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.", - "apiKeysInfo": "Votre clé API est :", - "apiKeysConfirmCopy": "J'ai copié la clé API", - "generate": "Générer", - "done": "Terminé", - "apiKeysSeeAll": "Voir toutes les clés API", - "apiKeysPermissionsErrorLoadingActions": "Erreur lors du chargement des actions de la clé API", - "apiKeysPermissionsErrorUpdate": "Erreur lors de la définition des permissions", - "apiKeysPermissionsUpdated": "Permissions mises à jour", - "apiKeysPermissionsUpdatedDescription": "Les permissions ont été mises à jour.", - "apiKeysPermissionsGeneralSettings": "Permissions", - "apiKeysPermissionsGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire", - "apiKeysPermissionsSave": "Enregistrer les permissions", - "apiKeysPermissionsTitle": "Permissions", - "apiKeys": "Clés API", - "searchApiKeys": "Rechercher des clés API...", - "apiKeysAdd": "Générer une clé API", - "apiKeysErrorDelete": "Erreur lors de la suppression de la clé API", - "apiKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé API", - "apiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé API {selectedApiKey} de l'organisation ?", - "apiKeysMessageRemove": "Une fois supprimée, la clé API ne pourra plus être utilisée.", - "apiKeysMessageConfirm": "Pour confirmer, veuillez saisir le nom de la clé API ci-dessous.", - "apiKeysDeleteConfirm": "Confirmer la suppression de la clé API", - "apiKeysDelete": "Supprimer la clé API", - "apiKeysManage": "Gérer les clés API", - "apiKeysDescription": "Les clés API sont utilisées pour s'authentifier avec l'API d'intégration", - "apiKeysSettings": "Paramètres de {apiKeyName}", - "userTitle": "Gérer tous les utilisateurs", - "userDescription": "Voir et gérer tous les utilisateurs du système", - "userAbount": "À propos de la gestion des utilisateurs", - "userAbountDescription": "Cette table affiche tous les objets utilisateur root du système. Chaque utilisateur peut appartenir à plusieurs organisations. La suppression d'un utilisateur d'une organisation ne supprime pas son objet utilisateur root - il restera dans le système. Pour supprimer complètement un utilisateur du système, vous devez supprimer son objet utilisateur root en utilisant l'action de suppression dans cette table.", - "userServer": "Utilisateurs du serveur", - "userSearch": "Rechercher des utilisateurs du serveur...", - "userErrorDelete": "Erreur lors de la suppression de l'utilisateur", - "userDeleteConfirm": "Confirmer la suppression de l'utilisateur", - "userDeleteServer": "Supprimer l'utilisateur du serveur", - "userMessageRemove": "L'utilisateur sera retiré de toutes les organisations et sera complètement retiré du serveur.", - "userMessageConfirm": "Pour confirmer, veuillez saisir le nom de l'utilisateur ci-dessous.", - "userQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement {selectedUser} du serveur?", - "licenseKey": "Clé de licence", - "valid": "Valide", - "numberOfSites": "Nombre de sites", - "licenseKeySearch": "Rechercher des clés de licence...", - "licenseKeyAdd": "Ajouter une clé de licence", - "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", - "licenseErrorKeyLoadDescription": "Une erreur s'est produite lors du chargement des clés de licence.", - "licenseErrorKeyDelete": "Échec de la suppression de la clé de licence", - "licenseErrorKeyDeleteDescription": "Une erreur s'est produite lors de la suppression de la clé de licence.", - "licenseKeyDeleted": "Clé de licence supprimée", - "licenseKeyDeletedDescription": "La clé de licence a été supprimée.", - "licenseErrorKeyActivate": "Échec de l'activation de la clé de licence", - "licenseErrorKeyActivateDescription": "Une erreur s'est produite lors de l'activation de la clé de licence.", - "licenseAbout": "À propos de la licence", - "communityEdition": "Edition Communautaire", - "licenseAboutDescription": "Ceci est destiné aux entreprises qui utilisent Pangolin dans un environnement commercial. Si vous utilisez Pangolin pour un usage personnel, vous pouvez ignorer cette section.", - "licenseKeyActivated": "Clé de licence activée", - "licenseKeyActivatedDescription": "La clé de licence a été activée avec succès.", - "licenseErrorKeyRecheck": "Impossible de revérifier les clés de licence", - "licenseErrorKeyRecheckDescription": "Une erreur s'est produite lors de la revérification des clés de licence.", - "licenseErrorKeyRechecked": "Clés de licence revérifiées", - "licenseErrorKeyRecheckedDescription": "Toutes les clés de licence ont été revérifiées", - "licenseActivateKey": "Activer la clé de licence", - "licenseActivateKeyDescription": "Entrez une clé de licence pour l'activer.", - "licenseActivate": "Activer la licence", - "licenseAgreement": "En cochant cette case, vous confirmez avoir lu et accepté les conditions de licence correspondant au niveau associé à votre clé de licence.", - "fossorialLicense": "Voir les conditions de licence commerciale et d'abonnement Fossorial", - "licenseMessageRemove": "Cela supprimera la clé de licence et toutes les autorisations qui lui sont associées.", - "licenseMessageConfirm": "Pour confirmer, veuillez saisir la clé de licence ci-dessous.", - "licenseQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé de licence {selectedKey}?", - "licenseKeyDelete": "Supprimer la clé de licence", - "licenseKeyDeleteConfirm": "Confirmer la suppression de la clé de licence", - "licenseTitle": "Gérer le statut de la licence", - "licenseTitleDescription": "Voir et gérer les clés de licence dans le système", - "licenseHost": "Licence Hôte", - "licenseHostDescription": "Gérer la clé de licence principale de l'hôte.", - "licensedNot": "Non licencié", - "hostId": "ID de l'hôte", - "licenseReckeckAll": "Revérifier toutes les clés", - "licenseSiteUsage": "Utilisation des sites", - "licenseSiteUsageDecsription": "Voir le nombre de sites utilisant cette licence.", - "licenseNoSiteLimit": "Il n'y a pas de limite sur le nombre de sites utilisant un hôte non autorisé.", - "licensePurchase": "Acheter une licence", - "licensePurchaseSites": "Acheter des sites supplémentaires", - "licenseSitesUsedMax": "{usedSites} des sites {maxSites} utilisés", - "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} dans le système.", - "licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}", - "licenseFee": "Frais de licence", - "licensePriceSite": "Prix par site", - "total": "Total", - "licenseContinuePayment": "Continuer vers le paiement", - "pricingPage": "page de tarification", - "pricingPortal": "Voir le portail d'achat", - "licensePricingPage": "Pour les prix et les remises les plus récentes, veuillez visiter le ", - "invite": "Invitations", - "inviteRegenerate": "Régénérer l'invitation", - "inviteRegenerateDescription": "Révoquer l'invitation précédente et en créer une nouvelle", - "inviteRemove": "Supprimer l'invitation", - "inviteRemoveError": "Échec de la suppression de l'invitation", - "inviteRemoveErrorDescription": "Une erreur s'est produite lors de la suppression de l'invitation.", - "inviteRemoved": "Invitation supprimée", - "inviteRemovedDescription": "L'invitation pour {email} a été supprimée.", - "inviteQuestionRemove": "Êtes-vous sûr de vouloir supprimer l'invitation {email}?", - "inviteMessageRemove": "Une fois supprimée, cette invitation ne sera plus valide. Vous pourrez toujours réinviter l'utilisateur plus tard.", - "inviteMessageConfirm": "Pour confirmer, veuillez saisir l'adresse e-mail de l'invitation ci-dessous.", - "inviteQuestionRegenerate": "Êtes-vous sûr de vouloir régénérer l'invitation {email}? Cela révoquera l'invitation précédente.", - "inviteRemoveConfirm": "Confirmer la suppression de l'invitation", - "inviteRegenerated": "Invitation régénérée", - "inviteSent": "Une nouvelle invitation a été envoyée à {email}.", - "inviteSentEmail": "Envoyer une notification par e-mail à l'utilisateur", - "inviteGenerate": "Une nouvelle invitation a été générée pour {email}.", - "inviteDuplicateError": "Invitation en double", - "inviteDuplicateErrorDescription": "Une invitation pour cet utilisateur existe déjà.", - "inviteRateLimitError": "Limite de taux dépassée", - "inviteRateLimitErrorDescription": "Vous avez dépassé la limite de 3 régénérations par heure. Veuillez réessayer plus tard.", - "inviteRegenerateError": "Échec de la régénération de l'invitation", - "inviteRegenerateErrorDescription": "Une erreur s'est produite lors de la régénération de l'invitation.", - "inviteValidityPeriod": "Période de validité", - "inviteValidityPeriodSelect": "Sélectionner la période de validité", - "inviteRegenerateMessage": "L'invitation a été régénérée. L'utilisateur doit accéder au lien ci-dessous pour accepter l'invitation.", - "inviteRegenerateButton": "Régénérer", - "expiresAt": "Expire le", - "accessRoleUnknown": "Rôle inconnu", - "placeholder": "Espace réservé", - "userErrorOrgRemove": "Échec de la suppression de l'utilisateur", - "userErrorOrgRemoveDescription": "Une erreur s'est produite lors de la suppression de l'utilisateur.", - "userOrgRemoved": "Utilisateur supprimé", - "userOrgRemovedDescription": "L'utilisateur {email} a été retiré de l'organisation.", - "userQuestionOrgRemove": "Êtes-vous sûr de vouloir retirer {email} de l'organisation ?", - "userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.", - "userMessageOrgConfirm": "Pour confirmer, veuillez saisir le nom de l'utilisateur ci-dessous.", - "userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur", - "userRemoveOrg": "Retirer l'utilisateur de l'organisation", - "users": "Utilisateurs", - "accessRoleMember": "Membre", - "accessRoleOwner": "Propriétaire", - "userConfirmed": "Confirmé", - "idpNameInternal": "Interne", - "emailInvalid": "Adresse e-mail invalide", - "inviteValidityDuration": "Veuillez sélectionner une durée", - "accessRoleSelectPlease": "Veuillez sélectionner un rôle", - "usernameRequired": "Le nom d'utilisateur est requis", - "idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité", - "idpGenericOidc": "Fournisseur OAuth2/OIDC générique.", - "accessRoleErrorFetch": "Échec de la récupération des rôles", - "accessRoleErrorFetchDescription": "Une erreur s'est produite lors de la récupération des rôles", - "idpErrorFetch": "Échec de la récupération des fournisseurs d'identité", - "idpErrorFetchDescription": "Une erreur s'est produite lors de la récupération des fournisseurs d'identité", - "userErrorExists": "L'utilisateur existe déjà", - "userErrorExistsDescription": "Cet utilisateur est déjà membre de l'organisation.", - "inviteError": "Échec de l'invitation de l'utilisateur", - "inviteErrorDescription": "Une erreur s'est produite lors de l'invitation de l'utilisateur", - "userInvited": "Utilisateur invité", - "userInvitedDescription": "L'utilisateur a été invité avec succès.", - "userErrorCreate": "Échec de la création de l'utilisateur", - "userErrorCreateDescription": "Une erreur s'est produite lors de la création de l'utilisateur", - "userCreated": "Utilisateur créé", - "userCreatedDescription": "L'utilisateur a été créé avec succès.", - "userTypeInternal": "Utilisateur interne", - "userTypeInternalDescription": "Inviter un utilisateur à rejoindre votre organisation directement.", - "userTypeExternal": "Utilisateur externe", - "userTypeExternalDescription": "Créer un utilisateur avec un fournisseur d'identité externe.", - "accessUserCreateDescription": "Suivez les étapes ci-dessous pour créer un nouvel utilisateur", - "userSeeAll": "Voir tous les utilisateurs", - "userTypeTitle": "Type d'utilisateur", - "userTypeDescription": "Déterminez comment vous voulez créer l'utilisateur", - "userSettings": "Informations utilisateur", - "userSettingsDescription": "Entrez les détails du nouvel utilisateur", - "inviteEmailSent": "Envoyer un e-mail d'invitation à l'utilisateur", - "inviteValid": "Valide pour", - "selectDuration": "Sélectionner la durée", - "accessRoleSelect": "Sélectionner un rôle", - "inviteEmailSentDescription": "Un e-mail a été envoyé à l'utilisateur avec le lien d'accès ci-dessous. Ils doivent accéder au lien pour accepter l'invitation.", - "inviteSentDescription": "L'utilisateur a été invité. Ils doivent accéder au lien ci-dessous pour accepter l'invitation.", - "inviteExpiresIn": "L'invitation expirera dans {days, plural, one {# jour} other {# jours}}.", - "idpTitle": "Informations générales", - "idpSelect": "Sélectionnez le fournisseur d'identité pour l'utilisateur externe", - "idpNotConfigured": "Aucun fournisseur d'identité n'est configuré. Veuillez configurer un fournisseur d'identité avant de créer des utilisateurs externes.", - "usernameUniq": "Ceci doit correspondre au nom d'utilisateur unique qui existe dans le fournisseur d'identité sélectionné.", - "emailOptional": "E-mail (Optionnel)", - "nameOptional": "Nom (Optionnel)", - "accessControls": "Contrôles d'accès", - "userDescription2": "Gérer les paramètres de cet utilisateur", - "accessRoleErrorAdd": "Échec de l'ajout de l'utilisateur au rôle", - "accessRoleErrorAddDescription": "Une erreur s'est produite lors de l'ajout de l'utilisateur au rôle.", - "userSaved": "Utilisateur enregistré", - "userSavedDescription": "L'utilisateur a été mis à jour.", - "autoProvisioned": "Auto-provisionné", - "autoProvisionedDescription": "Permettre à cet utilisateur d'être géré automatiquement par le fournisseur d'identité", - "accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation", - "accessControlsSubmit": "Enregistrer les contrôles d'accès", - "roles": "Rôles", - "accessUsersRoles": "Gérer les utilisateurs et les rôles", - "accessUsersRolesDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation", - "key": "Clé", - "createdAt": "Créé le", - "proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.", - "proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.", - "proxyEnableSSL": "Activer SSL", - "proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.", - "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", - "siteErrorFetchDescription": "Une erreur s'est produite lors de la récupération de la ressource", - "targetErrorDuplicate": "Cible en double", - "targetErrorDuplicateDescription": "Une cible avec ces paramètres existe déjà", - "targetWireGuardErrorInvalidIp": "IP cible invalide", - "targetWireGuardErrorInvalidIpDescription": "L'IP cible doit être dans le sous-réseau du site", - "targetsUpdated": "Cibles mises à jour", - "targetsUpdatedDescription": "Cibles et paramètres mis à jour avec succès", - "targetsErrorUpdate": "Échec de la mise à jour des cibles", - "targetsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des cibles", - "targetTlsUpdate": "Paramètres TLS mis à jour", - "targetTlsUpdateDescription": "Vos paramètres TLS ont été mis à jour avec succès", - "targetErrorTlsUpdate": "Échec de la mise à jour des paramètres TLS", - "targetErrorTlsUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres TLS", - "proxyUpdated": "Paramètres du proxy mis à jour", - "proxyUpdatedDescription": "Vos paramètres de proxy ont été mis à jour avec succès", - "proxyErrorUpdate": "Échec de la mise à jour des paramètres du proxy", - "proxyErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres du proxy", - "targetAddr": "IP / Nom d'hôte", - "targetPort": "Port", - "targetProtocol": "Protocole", - "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 du serveur TLS", - "targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.", - "targetTlsSubmit": "Enregistrer les paramètres", - "targets": "Configuration des cibles", - "targetsDescription": "Configurez les cibles pour router le trafic vers vos services.", - "targetStickySessions": "Activer les sessions persistantes", - "targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.", - "methodSelect": "Sélectionner la méthode", - "targetSubmit": "Ajouter une cible", - "targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.", - "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é", - "proxyCustomHeaderDescription": "L'en-tête host à définir lors du proxy des requêtes. Laissez vide pour utiliser la valeur par défaut.", - "proxyAdditionalSubmit": "Enregistrer les paramètres de proxy", - "subnetMaskErrorInvalid": "Masque de sous-réseau invalide. Doit être entre 0 et 32.", - "ipAddressErrorInvalidFormat": "Format d'adresse IP invalide", - "ipAddressErrorInvalidOctet": "Octet d'adresse IP invalide", - "path": "Chemin", - "matchPath": "Chemin de correspondance", - "ipAddressRange": "Plage IP", - "rulesErrorFetch": "Échec de la récupération des règles", - "rulesErrorFetchDescription": "Une erreur s'est produite lors de la récupération des règles", - "rulesErrorDuplicate": "Règle en double", - "rulesErrorDuplicateDescription": "Une règle avec ces paramètres existe déjà", - "rulesErrorInvalidIpAddressRange": "CIDR invalide", - "rulesErrorInvalidIpAddressRangeDescription": "Veuillez entrer une valeur CIDR valide", - "rulesErrorInvalidUrl": "Chemin URL invalide", - "rulesErrorInvalidUrlDescription": "Veuillez entrer un chemin URL valide", - "rulesErrorInvalidIpAddress": "IP invalide", - "rulesErrorInvalidIpAddressDescription": "Veuillez entrer une adresse IP valide", - "rulesErrorUpdate": "Échec de la mise à jour des règles", - "rulesErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des règles", - "rulesUpdated": "Activer les règles", - "rulesUpdatedDescription": "L'évaluation des règles a été mise à jour", - "rulesMatchIpAddressRangeDescription": "Entrez une adresse au format CIDR (ex: 103.21.244.0/22)", - "rulesMatchIpAddress": "Entrez une adresse IP (ex: 103.21.244.12)", - "rulesMatchUrl": "Entrez un chemin URL ou un motif (ex: /api/v1/todos ou /api/v1/*)", - "rulesErrorInvalidPriority": "Priorité invalide", - "rulesErrorInvalidPriorityDescription": "Veuillez entrer une priorité valide", - "rulesErrorDuplicatePriority": "Priorités en double", - "rulesErrorDuplicatePriorityDescription": "Veuillez entrer des priorités uniques", - "ruleUpdated": "Règles mises à jour", - "ruleUpdatedDescription": "Règles mises à jour avec succès", - "ruleErrorUpdate": "L'opération a échoué", - "ruleErrorUpdateDescription": "Une erreur s'est produite lors de l'enregistrement", - "rulesPriority": "Priorité", - "rulesAction": "Action", - "rulesMatchType": "Type de correspondance", - "value": "Valeur", - "rulesAbout": "À propos des règles", - "rulesAboutDescription": "Les règles vous permettent de contrôler l'accès à votre ressource en fonction d'un ensemble de critères. Vous pouvez créer des règles pour autoriser ou refuser l'accès basé sur l'adresse IP ou le chemin URL.", - "rulesActions": "Actions", - "rulesActionAlwaysAllow": "Toujours autoriser : Contourner toutes les méthodes d'authentification", - "rulesActionAlwaysDeny": "Toujours refuser : Bloquer toutes les requêtes ; aucune authentification ne peut être tentée", - "rulesActionPassToAuth": "Passer à l'authentification : Autoriser les méthodes d'authentification à être tentées", - "rulesMatchCriteria": "Critères de correspondance", - "rulesMatchCriteriaIpAddress": "Correspondre à une adresse IP spécifique", - "rulesMatchCriteriaIpAddressRange": "Correspondre à une plage d'adresses IP en notation CIDR", - "rulesMatchCriteriaUrl": "Correspondre à un chemin URL ou un motif", - "rulesEnable": "Activer les règles", - "rulesEnableDescription": "Activer ou désactiver l'évaluation des règles pour cette ressource", - "rulesResource": "Configuration des règles de ressource", - "rulesResourceDescription": "Configurer les règles pour contrôler l'accès à votre ressource", - "ruleSubmit": "Ajouter une règle", - "rulesNoOne": "Aucune règle. Ajoutez une règle en utilisant le formulaire.", - "rulesOrder": "Les règles sont évaluées par priorité dans l'ordre croissant.", - "rulesSubmit": "Enregistrer les règles", - "resourceErrorCreate": "Erreur lors de la création de la ressource", - "resourceErrorCreateDescription": "Une erreur s'est produite lors de la création de la ressource", - "resourceErrorCreateMessage": "Erreur lors de la création de la ressource :", - "resourceErrorCreateMessageDescription": "Une erreur inattendue s'est produite", - "sitesErrorFetch": "Erreur lors de la récupération des sites", - "sitesErrorFetchDescription": "Une erreur s'est produite lors de la récupération des sites", - "domainsErrorFetch": "Erreur lors de la récupération des domaines", - "domainsErrorFetchDescription": "Une erreur s'est produite lors de la récupération des domaines", - "none": "Aucun", - "unknown": "Inconnu", - "resources": "Ressources", - "resourcesDescription": "Les ressources sont des proxys vers des applications exécutées sur votre réseau privé. Créez une ressource pour tout service HTTP/HTTPS ou TCP/UDP brut sur votre réseau privé. Chaque ressource doit être connectée à un site pour permettre une connectivité privée et sécurisée via un tunnel WireGuard chiffré.", - "resourcesWireGuardConnect": "Connectivité sécurisée avec chiffrement WireGuard", - "resourcesMultipleAuthenticationMethods": "Configurer plusieurs méthodes d'authentification", - "resourcesUsersRolesAccess": "Contrôle d'accès basé sur les utilisateurs et les rôles", - "resourcesErrorUpdate": "Échec de la bascule de la ressource", - "resourcesErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour de la ressource", - "access": "Accès", - "shareLink": "Lien de partage {resource}", - "resourceSelect": "Sélectionner une ressource", - "shareLinks": "Liens de partage", - "share": "Liens partageables", - "shareDescription2": "Créez des liens partageables vers vos ressources. Les liens fournissent un accès temporaire ou illimité à votre ressource. Vous pouvez configurer la durée d'expiration du lien lors de sa création.", - "shareEasyCreate": "Facile à créer et à partager", - "shareConfigurableExpirationDuration": "Durée d'expiration configurable", - "shareSecureAndRevocable": "Sécurisé et révocable", - "nameMin": "Le nom doit comporter au moins {len} caractères.", - "nameMax": "Le nom ne doit pas dépasser {len} caractères.", - "sitesConfirmCopy": "Veuillez confirmer que vous avez copié la configuration.", - "unknownCommand": "Commande inconnue", - "newtErrorFetchReleases": "Échec de la récupération des informations de version : {err}", - "newtErrorFetchLatest": "Erreur lors de la récupération de la dernière version : {err}", - "newtEndpoint": "Point de terminaison Newt", - "newtId": "ID Newt", - "newtSecretKey": "Clé secrète Newt", - "architecture": "Architecture", - "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", - "userErrorNotAdminOrOwner": "L'utilisateur n'est pas un administrateur ou un propriétaire", - "pangolinSettings": "Paramètres - Pangolin", - "accessRoleYour": "Votre rôle :", - "accessRoleSelect2": "Sélectionner un rôle", - "accessUserSelect": "Sélectionner un utilisateur", - "otpEmailEnter": "Entrer un e-mail", - "otpEmailEnterDescription": "Appuyez sur Entrée pour ajouter un e-mail après l'avoir saisi dans le champ.", - "otpEmailErrorInvalid": "Adresse e-mail invalide. Le caractère générique (*) doit être la partie locale entière.", - "otpEmailSmtpRequired": "SMTP requis", - "otpEmailSmtpRequiredDescription": "Le SMTP doit être activé sur le serveur pour utiliser l'authentification par mot de passe à usage unique.", - "otpEmailTitle": "Mots de passe à usage unique", - "otpEmailTitleDescription": "Exiger une authentification par e-mail pour l'accès aux ressources", - "otpEmailWhitelist": "Liste blanche des e-mails", - "otpEmailWhitelistList": "E-mails sur liste blanche", - "otpEmailWhitelistListDescription": "Seuls les utilisateurs avec ces adresses e-mail pourront accéder à cette ressource. Ils devront saisir un mot de passe à usage unique envoyé à leur e-mail. Les caractères génériques (*@example.com) peuvent être utilisés pour autoriser n'importe quelle adresse e-mail d'un domaine.", - "otpEmailWhitelistSave": "Enregistrer la liste blanche", - "passwordAdd": "Ajouter un mot de passe", - "passwordRemove": "Supprimer le mot de passe", - "pincodeAdd": "Ajouter un code PIN", - "pincodeRemove": "Supprimer le code PIN", - "resourceAuthMethods": "Méthodes d'authentification", - "resourceAuthMethodsDescriptions": "Permettre l'accès à la ressource via des méthodes d'authentification supplémentaires", - "resourceAuthSettingsSave": "Enregistré avec succès", - "resourceAuthSettingsSaveDescription": "Les paramètres d'authentification ont été enregistrés", - "resourceErrorAuthFetch": "Échec de la récupération des données", - "resourceErrorAuthFetchDescription": "Une erreur s'est produite lors de la récupération des données", - "resourceErrorPasswordRemove": "Erreur lors de la suppression du mot de passe de la ressource", - "resourceErrorPasswordRemoveDescription": "Une erreur s'est produite lors de la suppression du mot de passe de la ressource", - "resourceErrorPasswordSetup": "Erreur lors de la configuration du mot de passe de la ressource", - "resourceErrorPasswordSetupDescription": "Une erreur s'est produite lors de la configuration du mot de passe de la ressource", - "resourceErrorPincodeRemove": "Erreur lors de la suppression du code PIN de la ressource", - "resourceErrorPincodeRemoveDescription": "Une erreur s'est produite lors de la suppression du code PIN de la ressource", - "resourceErrorPincodeSetup": "Erreur lors de la configuration du code PIN de la ressource", - "resourceErrorPincodeSetupDescription": "Une erreur s'est produite lors de la configuration du code PIN de la ressource", - "resourceErrorUsersRolesSave": "Échec de la définition des rôles", - "resourceErrorUsersRolesSaveDescription": "Une erreur s'est produite lors de la définition des rôles", - "resourceErrorWhitelistSave": "Échec de l'enregistrement de la liste blanche", - "resourceErrorWhitelistSaveDescription": "Une erreur s'est produite lors de l'enregistrement de la liste blanche", - "resourcePasswordSubmit": "Activer la protection par mot de passe", - "resourcePasswordProtection": "Protection par mot de passe {status}", - "resourcePasswordRemove": "Mot de passe de la ressource supprimé", - "resourcePasswordRemoveDescription": "Le mot de passe de la ressource a été supprimé avec succès", - "resourcePasswordSetup": "Mot de passe de la ressource défini", - "resourcePasswordSetupDescription": "Le mot de passe de la ressource a été défini avec succès", - "resourcePasswordSetupTitle": "Définir le mot de passe", - "resourcePasswordSetupTitleDescription": "Définir un mot de passe pour protéger cette ressource", - "resourcePincode": "Code PIN", - "resourcePincodeSubmit": "Activer la protection par code PIN", - "resourcePincodeProtection": "Protection par code PIN {status}", - "resourcePincodeRemove": "Code PIN de la ressource supprimé", - "resourcePincodeRemoveDescription": "Le code PIN de la ressource a été supprimé avec succès", - "resourcePincodeSetup": "Code PIN de la ressource défini", - "resourcePincodeSetupDescription": "Le code PIN de la ressource a été défini avec succès", - "resourcePincodeSetupTitle": "Définir le code PIN", - "resourcePincodeSetupTitleDescription": "Définir un code PIN pour protéger cette ressource", - "resourceRoleDescription": "Les administrateurs peuvent toujours accéder à cette ressource.", - "resourceUsersRoles": "Utilisateurs et rôles", - "resourceUsersRolesDescription": "Configurer quels utilisateurs et rôles peuvent visiter cette ressource", - "resourceUsersRolesSubmit": "Enregistrer les utilisateurs et les rôles", - "resourceWhitelistSave": "Enregistré avec succès", - "resourceWhitelistSaveDescription": "Les paramètres de la liste blanche ont été enregistrés", - "ssoUse": "Utiliser la SSO de la plateforme", - "ssoUseDescription": "Les utilisateurs existants n'auront à se connecter qu'une seule fois pour toutes les ressources qui ont cette option activée.", - "proxyErrorInvalidPort": "Numéro de port invalide", - "subdomainErrorInvalid": "Sous-domaine invalide", - "domainErrorFetch": "Erreur lors de la récupération des domaines", - "domainErrorFetchDescription": "Une erreur s'est produite lors de la récupération des domaines", - "resourceErrorUpdate": "Échec de la mise à jour de la ressource", - "resourceErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour de la ressource", - "resourceUpdated": "Ressource mise à jour", - "resourceUpdatedDescription": "La ressource a été mise à jour avec succès", - "resourceErrorTransfer": "Échec du transfert de la ressource", - "resourceErrorTransferDescription": "Une erreur s'est produite lors du transfert de la ressource", - "resourceTransferred": "Ressource transférée", - "resourceTransferredDescription": "La ressource a été transférée avec succès", - "resourceErrorToggle": "Échec de la modification de l'état de la ressource", - "resourceErrorToggleDescription": "Une erreur s'est produite lors de la mise à jour de la ressource", - "resourceVisibilityTitle": "Visibilité", - "resourceVisibilityTitleDescription": "Activer ou désactiver complètement la visibilité de la ressource", - "resourceGeneral": "Paramètres généraux", - "resourceGeneralDescription": "Configurer les paramètres généraux de cette ressource", - "resourceEnable": "Activer la ressource", - "resourceTransfer": "Transférer la ressource", - "resourceTransferDescription": "Transférer cette ressource vers un autre site", - "resourceTransferSubmit": "Transférer la ressource", - "siteDestination": "Site de destination", - "searchSites": "Rechercher des sites", - "accessRoleCreate": "Créer un rôle", - "accessRoleCreateDescription": "Créer un nouveau rôle pour regrouper les utilisateurs et gérer leurs permissions.", - "accessRoleCreateSubmit": "Créer un rôle", - "accessRoleCreated": "Rôle créé", - "accessRoleCreatedDescription": "Le rôle a été créé avec succès.", - "accessRoleErrorCreate": "Échec de la création du rôle", - "accessRoleErrorCreateDescription": "Une erreur s'est produite lors de la création du rôle.", - "accessRoleErrorNewRequired": "Un nouveau rôle est requis", - "accessRoleErrorRemove": "Échec de la suppression du rôle", - "accessRoleErrorRemoveDescription": "Une erreur s'est produite lors de la suppression du rôle.", - "accessRoleName": "Nom du rôle", - "accessRoleQuestionRemove": "Vous êtes sur le point de supprimer le rôle {name}. Cette action est irréversible.", - "accessRoleRemove": "Supprimer le rôle", - "accessRoleRemoveDescription": "Retirer un rôle de l'organisation", - "accessRoleRemoveSubmit": "Supprimer le rôle", - "accessRoleRemoved": "Rôle supprimé", - "accessRoleRemovedDescription": "Le rôle a été supprimé avec succès.", - "accessRoleRequiredRemove": "Avant de supprimer ce rôle, veuillez sélectionner un nouveau rôle pour transférer les membres existants.", - "manage": "Gérer", - "sitesNotFound": "Aucun site trouvé.", - "pangolinServerAdmin": "Admin Serveur - Pangolin", - "licenseTierProfessional": "Licence Professionnelle", - "licenseTierEnterprise": "Licence Entreprise", - "licenseTierPersonal": "Personal License", - "licensed": "Sous licence", - "yes": "Oui", - "no": "Non", - "sitesAdditional": "Sites supplémentaires", - "licenseKeys": "Clés de licence", - "sitestCountDecrease": "Diminuer le nombre de sites", - "sitestCountIncrease": "Augmenter le nombre de sites", - "idpManage": "Gérer les fournisseurs d'identité", - "idpManageDescription": "Voir et gérer les fournisseurs d'identité dans le système", - "idpDeletedDescription": "Fournisseur d'identité supprimé avec succès", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement le fournisseur d'identité {name}?", - "idpMessageRemove": "Cela supprimera le fournisseur d'identité et toutes les configurations associées. Les utilisateurs qui s'authentifient via ce fournisseur ne pourront plus se connecter.", - "idpMessageConfirm": "Pour confirmer, veuillez saisir le nom du fournisseur d'identité ci-dessous.", - "idpConfirmDelete": "Confirmer la suppression du fournisseur d'identité", - "idpDelete": "Supprimer le fournisseur d'identité", - "idp": "Fournisseurs d'identité", - "idpSearch": "Rechercher des fournisseurs d'identité...", - "idpAdd": "Ajouter un fournisseur d'identité", - "idpClientIdRequired": "L'ID client est requis.", - "idpClientSecretRequired": "Le secret client est requis.", - "idpErrorAuthUrlInvalid": "L'URL d'authentification doit être une URL valide.", - "idpErrorTokenUrlInvalid": "L'URL du jeton doit être une URL valide.", - "idpPathRequired": "Le chemin d'identification est requis.", - "idpScopeRequired": "Les portées sont requises.", - "idpOidcDescription": "Configurer un fournisseur d'identité OpenID Connect", - "idpCreatedDescription": "Fournisseur d'identité créé avec succès", - "idpCreate": "Créer un fournisseur d'identité", - "idpCreateDescription": "Configurer un nouveau fournisseur d'identité pour l'authentification des utilisateurs", - "idpSeeAll": "Voir tous les fournisseurs d'identité", - "idpSettingsDescription": "Configurer les informations de base de votre fournisseur d'identité", - "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": "EE", - "idpType": "Type de fournisseur", - "idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer", - "idpOidcConfigure": "Configuration OAuth2/OIDC", - "idpOidcConfigureDescription": "Configurer les points de terminaison et les identifiants du fournisseur OAuth2/OIDC", - "idpClientId": "ID Client", - "idpClientIdDescription": "L'ID client OAuth2 de votre fournisseur d'identité", - "idpClientSecret": "Secret Client", - "idpClientSecretDescription": "Le secret client OAuth2 de votre fournisseur d'identité", - "idpAuthUrl": "URL d'autorisation", - "idpAuthUrlDescription": "L'URL du point de terminaison d'autorisation OAuth2", - "idpTokenUrl": "URL du jeton", - "idpTokenUrlDescription": "L'URL du point de terminaison du jeton OAuth2", - "idpOidcConfigureAlert": "Information importante", - "idpOidcConfigureAlertDescription": "Après avoir créé le fournisseur d'identité, vous devrez configurer l'URL de rappel dans les paramètres de votre fournisseur d'identité. L'URL de rappel sera fournie après la création réussie.", - "idpToken": "Configuration du jeton", - "idpTokenDescription": "Configurer comment extraire les informations utilisateur du jeton ID", - "idpJmespathAbout": "À propos de JMESPath", - "idpJmespathAboutDescription": "Les chemins ci-dessous utilisent la syntaxe JMESPath pour extraire des valeurs du jeton ID.", - "idpJmespathAboutDescriptionLink": "En savoir plus sur JMESPath", - "idpJmespathLabel": "Chemin d'identification", - "idpJmespathLabelDescription": "Le JMESPath vers l'identifiant de l'utilisateur dans le jeton ID", - "idpJmespathEmailPathOptional": "Chemin de l'email (Optionnel)", - "idpJmespathEmailPathOptionalDescription": "Le JMESPath vers l'email de l'utilisateur dans le jeton ID", - "idpJmespathNamePathOptional": "Chemin du nom (Optionnel)", - "idpJmespathNamePathOptionalDescription": "Le JMESPath vers le nom de l'utilisateur dans le jeton ID", - "idpOidcConfigureScopes": "Portées", - "idpOidcConfigureScopesDescription": "Liste des portées OAuth2 à demander, séparées par des espaces", - "idpSubmit": "Créer le fournisseur d'identité", - "orgPolicies": "Politiques d'organisation", - "idpSettings": "Paramètres de {idpName}", - "idpCreateSettingsDescription": "Configurer les paramètres de votre fournisseur d'identité", - "roleMapping": "Mappage des rôles", - "orgMapping": "Mappage d'organisation", - "orgPoliciesSearch": "Rechercher des politiques d'organisation...", - "orgPoliciesAdd": "Ajouter une politique d'organisation", - "orgRequired": "L'organisation est requise", - "error": "Erreur", - "success": "Succès", - "orgPolicyAddedDescription": "Politique ajoutée avec succès", - "orgPolicyUpdatedDescription": "Politique mise à jour avec succès", - "orgPolicyDeletedDescription": "Politique supprimée avec succès", - "defaultMappingsUpdatedDescription": "Mappages par défaut mis à jour avec succès", - "orgPoliciesAbout": "À propos des politiques d'organisation", - "orgPoliciesAboutDescription": "Les politiques d'organisation sont utilisées pour contrôler l'accès aux organisations en fonction du jeton ID de l'utilisateur. Vous pouvez spécifier des expressions JMESPath pour extraire les informations de rôle et d'organisation du jeton ID. Pour plus d'informations, voir", - "orgPoliciesAboutDescriptionLink": "la documentation", - "defaultMappingsOptional": "Mappages par défaut (Optionnel)", - "defaultMappingsOptionalDescription": "Les mappages par défaut sont utilisés lorsqu'il n'y a pas de politique d'organisation définie pour une organisation. Vous pouvez spécifier ici les mappages de rôle et d'organisation par défaut à utiliser.", - "defaultMappingsRole": "Mappage de rôle par défaut", - "defaultMappingsRoleDescription": "JMESPath pour extraire les informations de rôle du jeton ID. Le résultat de cette expression doit renvoyer le nom du rôle tel que défini dans l'organisation sous forme de chaîne.", - "defaultMappingsOrg": "Mappage d'organisation par défaut", - "defaultMappingsOrgDescription": "JMESPath pour extraire les informations d'organisation du jeton ID. Cette expression doit renvoyer l'ID de l'organisation ou true pour que l'utilisateur soit autorisé à accéder à l'organisation.", - "defaultMappingsSubmit": "Enregistrer les mappages par défaut", - "orgPoliciesEdit": "Modifier la politique d'organisation", - "org": "Organisation", - "orgSelect": "Sélectionner une organisation", - "orgSearch": "Rechercher une organisation", - "orgNotFound": "Aucune organisation trouvée.", - "roleMappingPathOptional": "Chemin de mappage des rôles (Optionnel)", - "orgMappingPathOptional": "Chemin de mappage d'organisation (Optionnel)", - "orgPolicyUpdate": "Mettre à jour la politique", - "orgPolicyAdd": "Ajouter une politique", - "orgPolicyConfig": "Configurer l'accès pour une organisation", - "idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès", - "redirectUrl": "URL de redirection", - "redirectUrlAbout": "À propos de l'URL de redirection", - "redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres de votre fournisseur d'identité.", - "pangolinAuth": "Auth - Pangolin", - "verificationCodeLengthRequirements": "Votre code de vérification doit comporter 8 caractères.", - "errorOccurred": "Une erreur s'est produite", - "emailErrorVerify": "Échec de la vérification de l'e-mail :", - "emailVerified": "E-mail vérifié avec succès ! Redirection...", - "verificationCodeErrorResend": "Échec du renvoi du code de vérification :", - "verificationCodeResend": "Code de vérification renvoyé", - "verificationCodeResendDescription": "Nous avons renvoyé un code de vérification à votre adresse e-mail. Veuillez vérifier votre boîte de réception.", - "emailVerify": "Vérifier l'e-mail", - "emailVerifyDescription": "Entrez le code de vérification envoyé à votre adresse e-mail.", - "verificationCode": "Code de vérification", - "verificationCodeEmailSent": "Nous avons envoyé un code de vérification à votre adresse e-mail.", - "submit": "Soumettre", - "emailVerifyResendProgress": "Renvoi en cours...", - "emailVerifyResend": "Vous n'avez pas reçu de code ? Cliquez ici pour renvoyer", - "passwordNotMatch": "Les mots de passe ne correspondent pas", - "signupError": "Une erreur s'est produite lors de l'inscription", - "pangolinLogoAlt": "Logo Pangolin", - "inviteAlready": "On dirait que vous avez été invité !", - "inviteAlreadyDescription": "Pour accepter l'invitation, vous devez vous connecter ou créer un compte.", - "signupQuestion": "Vous avez déjà un compte ?", - "login": "Se connecter", - "resourceNotFound": "Ressource introuvable", - "resourceNotFoundDescription": "La ressource que vous essayez d'accéder n'existe pas.", - "pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres", - "pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres", - "passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère", - "passwordRequirementsTitle": "Exigences relatives au mot de passe :", - "passwordRequirementLength": "Au moins 8 caractères", - "passwordRequirementUppercase": "Au moins une lettre majuscule", - "passwordRequirementLowercase": "Au moins une lettre minuscule", - "passwordRequirementNumber": "Au moins un chiffre", - "passwordRequirementSpecial": "Au moins un caractère spécial", - "passwordRequirementsMet": "✓ Le mot de passe répond à toutes les exigences", - "passwordStrength": "Solidité du mot de passe", - "passwordStrengthWeak": "Faible", - "passwordStrengthMedium": "Moyen", - "passwordStrengthStrong": "Fort", - "passwordRequirements": "Exigences :", - "passwordRequirementLengthText": "8+ caractères", - "passwordRequirementUppercaseText": "Lettre majuscule (A-Z)", - "passwordRequirementLowercaseText": "Lettre minuscule (a-z)", - "passwordRequirementNumberText": "Nombre (0-9)", - "passwordRequirementSpecialText": "Caractère spécial (!@#$%...)", - "passwordsDoNotMatch": "Les mots de passe ne correspondent pas", - "otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère", - "otpEmailSent": "OTP envoyé", - "otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail", - "otpEmailErrorAuthenticate": "Échec de l'authentification par e-mail", - "pincodeErrorAuthenticate": "Échec de l'authentification avec le code PIN", - "passwordErrorAuthenticate": "Échec de l'authentification avec le mot de passe", - "poweredBy": "Propulsé par", - "authenticationRequired": "Authentification requise", - "authenticationMethodChoose": "Choisissez votre méthode préférée pour accéder à {name}", - "authenticationRequest": "Vous devez vous authentifier pour accéder à {name}", - "user": "Utilisateur", - "pincodeInput": "Code PIN à 6 chiffres", - "pincodeSubmit": "Se connecter avec le PIN", - "passwordSubmit": "Se connecter avec le mot de passe", - "otpEmailDescription": "Un code à usage unique sera envoyé à cet e-mail.", - "otpEmailSend": "Envoyer le code à usage unique", - "otpEmail": "Mot de passe à usage unique (OTP)", - "otpEmailSubmit": "Soumettre l'OTP", - "backToEmail": "Retour à l'e-mail", - "noSupportKey": "Le serveur fonctionne sans clé de supporteur. Pensez à soutenir le projet !", - "accessDenied": "Accès refusé", - "accessDeniedDescription": "Vous n'êtes pas autorisé à accéder à cette ressource. Si c'est une erreur, veuillez contacter l'administrateur.", - "accessTokenError": "Erreur lors de la vérification du jeton d'accès", - "accessGranted": "Accès accordé", - "accessUrlInvalid": "URL d'accès invalide", - "accessGrantedDescription": "L'accès à cette ressource vous a été accordé. Redirection...", - "accessUrlInvalidDescription": "Cette URL d'accès partagé n'est pas valide. Veuillez contacter le propriétaire de la ressource pour obtenir une nouvelle URL.", - "tokenInvalid": "Jeton invalide", - "pincodeInvalid": "Code invalide", - "passwordErrorRequestReset": "Échec de la demande de réinitialisation :", - "passwordErrorReset": "Échec de la réinitialisation du mot de passe :", - "passwordResetSuccess": "Mot de passe réinitialisé avec succès ! Retour à la connexion...", - "passwordReset": "Réinitialiser le mot de passe", - "passwordResetDescription": "Suivez les étapes pour réinitialiser votre mot de passe", - "passwordResetSent": "Nous allons envoyer un code de réinitialisation à cette adresse e-mail.", - "passwordResetCode": "Code de réinitialisation", - "passwordResetCodeDescription": "Vérifiez votre e-mail pour le code de réinitialisation.", - "passwordNew": "Nouveau mot de passe", - "passwordNewConfirm": "Confirmer le nouveau mot de passe", - "pincodeAuth": "Code d'authentification", - "pincodeSubmit2": "Soumettre le code", - "passwordResetSubmit": "Demander la réinitialisation", - "passwordBack": "Retour au mot de passe", - "loginBack": "Retour à la connexion", - "signup": "S'inscrire", - "loginStart": "Connectez-vous pour commencer", - "idpOidcTokenValidating": "Validation du jeton OIDC", - "idpOidcTokenResponse": "Valider la réponse du jeton OIDC", - "idpErrorOidcTokenValidating": "Erreur lors de la validation du jeton OIDC", - "idpConnectingTo": "Connexion à {name}", - "idpConnectingToDescription": "Validation de votre identité", - "idpConnectingToProcess": "Connexion...", - "idpConnectingToFinished": "Connecté", - "idpErrorConnectingTo": "Un problème est survenu lors de la connexion à {name}. Veuillez contacter votre administrateur.", - "idpErrorNotFound": "IdP introuvable", - "inviteInvalid": "Invitation invalide", - "inviteInvalidDescription": "Le lien d'invitation n'est pas valide.", - "inviteErrorWrongUser": "L'invitation n'est pas pour cet utilisateur", - "inviteErrorUserNotExists": "L'utilisateur n'existe pas. Veuillez d'abord créer un compte.", - "inviteErrorLoginRequired": "Vous devez être connecté pour accepter une invitation", - "inviteErrorExpired": "L'invitation a peut-être expiré", - "inviteErrorRevoked": "L'invitation a peut-être été révoquée", - "inviteErrorTypo": "Il pourrait y avoir une erreur de frappe dans le lien d'invitation", - "pangolinSetup": "Configuration - Pangolin", - "orgNameRequired": "Le nom de l'organisation est requis", - "orgIdRequired": "L'ID de l'organisation est requis", - "orgErrorCreate": "Une erreur s'est produite lors de la création de l'organisation", - "pageNotFound": "Page non trouvée", - "pageNotFoundDescription": "Oups! La page que vous recherchez n'existe pas.", - "overview": "Vue d'ensemble", - "home": "Accueil", - "accessControl": "Contrôle d'accès", - "settings": "Paramètres", - "usersAll": "Tous les utilisateurs", - "license": "Licence", - "pangolinDashboard": "Tableau de bord - Pangolin", - "noResults": "Aucun résultat trouvé.", - "terabytes": "{count} To", - "gigabytes": "{count} Go", - "megabytes": "{count} Mo", - "tagsEntered": "Tags saisis", - "tagsEnteredDescription": "Ce sont les tags que vous avez saisis.", - "tagsWarnCannotBeLessThanZero": "maxTags et minTags ne peuvent pas être inférieurs à 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag non autorisé selon les options d'autocomplétion", - "tagsWarnInvalid": "Tag invalide selon validateTag", - "tagWarnTooShort": "Le tag {tagText} est trop court", - "tagWarnTooLong": "Le tag {tagText} est trop long", - "tagsWarnReachedMaxNumber": "Nombre maximum de tags autorisés atteint", - "tagWarnDuplicate": "Tag en double {tagText} non ajouté", - "supportKeyInvalid": "Clé invalide", - "supportKeyInvalidDescription": "Votre clé de support est invalide.", - "supportKeyValid": "Clé valide", - "supportKeyValidDescription": "Votre clé de support a été validée. Merci pour votre soutien !", - "supportKeyErrorValidationDescription": "Échec de la validation de la clé de support.", - "supportKey": "Soutenez le développement et adoptez un Pangolin !", - "supportKeyDescription": "Achetez une clé de support pour nous aider à continuer le développement de Pangolin pour la communauté. Votre contribution nous permet de consacrer plus de temps à maintenir et ajouter de nouvelles fonctionnalités à l'application pour tous. Nous n'utiliserons jamais cela pour verrouiller des fonctionnalités. Ceci est distinct de toute Édition Commerciale.", - "supportKeyPet": "Vous pourrez aussi adopter et rencontrer votre propre Pangolin de compagnie !", - "supportKeyPurchase": "Les paiements sont traités via GitHub. Ensuite, vous pourrez récupérer votre clé sur", - "supportKeyPurchaseLink": "notre site web", - "supportKeyPurchase2": "et l'utiliser ici.", - "supportKeyLearnMore": "En savoir plus.", - "supportKeyOptions": "Veuillez sélectionner l'option qui vous convient le mieux.", - "supportKetOptionFull": "Support complet", - "forWholeServer": "Pour tout le serveur", - "lifetimePurchase": "Achat à vie", - "supporterStatus": "Statut de supporter", - "buy": "Acheter", - "supportKeyOptionLimited": "Support limité", - "forFiveUsers": "Pour 5 utilisateurs ou moins", - "supportKeyRedeem": "Utiliser une clé de support", - "supportKeyHideSevenDays": "Masquer pendant 7 jours", - "supportKeyEnter": "Saisir la clé de support", - "supportKeyEnterDescription": "Rencontrez votre propre Pangolin de compagnie !", - "githubUsername": "Nom d'utilisateur GitHub", - "supportKeyInput": "Clé de support", - "supportKeyBuy": "Acheter une clé de support", - "logoutError": "Erreur lors de la déconnexion", - "signingAs": "Connecté en tant que", - "serverAdmin": "Admin Serveur", - "managedSelfhosted": "Gestion autonome", - "otpEnable": "Activer l'authentification à deux facteurs", - "otpDisable": "Désactiver l'authentification à deux facteurs", - "logout": "Déconnexion", - "licenseTierProfessionalRequired": "Édition Professionnelle Requise", - "licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.", - "actionGetOrg": "Obtenir l'organisation", - "updateOrgUser": "Mise à jour de l'utilisateur Org", - "createOrgUser": "Créer un utilisateur Org", - "actionUpdateOrg": "Mettre à jour l'organisation", - "actionUpdateUser": "Mettre à jour l'utilisateur", - "actionGetUser": "Obtenir l'utilisateur", - "actionGetOrgUser": "Obtenir l'utilisateur de l'organisation", - "actionListOrgDomains": "Lister les domaines de l'organisation", - "actionCreateSite": "Créer un site", - "actionDeleteSite": "Supprimer un site", - "actionGetSite": "Obtenir un site", - "actionListSites": "Lister les sites", - "actionApplyBlueprint": "Appliquer le Plan", - "setupToken": "Jeton de configuration", - "setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.", - "setupTokenRequired": "Le jeton de configuration est requis.", - "actionUpdateSite": "Mettre à jour un site", - "actionListSiteRoles": "Lister les rôles autorisés du site", - "actionCreateResource": "Créer une ressource", - "actionDeleteResource": "Supprimer une ressource", - "actionGetResource": "Obtenir une ressource", - "actionListResource": "Lister les ressources", - "actionUpdateResource": "Mettre à jour une ressource", - "actionListResourceUsers": "Lister les utilisateurs de la ressource", - "actionSetResourceUsers": "Définir les utilisateurs de la ressource", - "actionSetAllowedResourceRoles": "Définir les rôles autorisés de la ressource", - "actionListAllowedResourceRoles": "Lister les rôles autorisés de la ressource", - "actionSetResourcePassword": "Définir le mot de passe de la ressource", - "actionSetResourcePincode": "Définir le code PIN de la ressource", - "actionSetResourceEmailWhitelist": "Définir la liste blanche des emails de la ressource", - "actionGetResourceEmailWhitelist": "Obtenir la liste blanche des emails de la ressource", - "actionCreateTarget": "Créer une cible", - "actionDeleteTarget": "Supprimer une cible", - "actionGetTarget": "Obtenir une cible", - "actionListTargets": "Lister les cibles", - "actionUpdateTarget": "Mettre à jour une cible", - "actionCreateRole": "Créer un rôle", - "actionDeleteRole": "Supprimer un rôle", - "actionGetRole": "Obtenir un rôle", - "actionListRole": "Lister les rôles", - "actionUpdateRole": "Mettre à jour un rôle", - "actionListAllowedRoleResources": "Lister les ressources autorisées du rôle", - "actionInviteUser": "Inviter un utilisateur", - "actionRemoveUser": "Supprimer un utilisateur", - "actionListUsers": "Lister les utilisateurs", - "actionAddUserRole": "Ajouter un rôle utilisateur", - "actionGenerateAccessToken": "Générer un jeton d'accès", - "actionDeleteAccessToken": "Supprimer un jeton d'accès", - "actionListAccessTokens": "Lister les jetons d'accès", - "actionCreateResourceRule": "Créer une règle de ressource", - "actionDeleteResourceRule": "Supprimer une règle de ressource", - "actionListResourceRules": "Lister les règles de ressource", - "actionUpdateResourceRule": "Mettre à jour une règle de ressource", - "actionListOrgs": "Lister les organisations", - "actionCheckOrgId": "Vérifier l'ID", - "actionCreateOrg": "Créer une organisation", - "actionDeleteOrg": "Supprimer une organisation", - "actionListApiKeys": "Lister les clés API", - "actionListApiKeyActions": "Lister les actions des clés API", - "actionSetApiKeyActions": "Définir les actions autorisées des clés API", - "actionCreateApiKey": "Créer une clé API", - "actionDeleteApiKey": "Supprimer une clé API", - "actionCreateIdp": "Créer un IDP", - "actionUpdateIdp": "Mettre à jour un IDP", - "actionDeleteIdp": "Supprimer un IDP", - "actionListIdps": "Lister les IDP", - "actionGetIdp": "Obtenir un IDP", - "actionCreateIdpOrg": "Créer une politique d'organisation IDP", - "actionDeleteIdpOrg": "Supprimer une politique d'organisation IDP", - "actionListIdpOrgs": "Lister les organisations IDP", - "actionUpdateIdpOrg": "Mettre à jour une organisation IDP", - "actionCreateClient": "Créer un client", - "actionDeleteClient": "Supprimer le client", - "actionUpdateClient": "Mettre à jour le client", - "actionListClients": "Liste des clients", - "actionGetClient": "Obtenir le client", - "actionCreateSiteResource": "Créer une ressource de site", - "actionDeleteSiteResource": "Supprimer une ressource de site", - "actionGetSiteResource": "Obtenir une ressource de site", - "actionListSiteResources": "Lister les ressources de site", - "actionUpdateSiteResource": "Mettre à jour une ressource de site", - "actionListInvitations": "Lister les invitations", - "noneSelected": "Aucune sélection", - "orgNotFound2": "Aucune organisation trouvée.", - "searchProgress": "Rechercher...", - "create": "Créer", - "orgs": "Organisations", - "loginError": "Une erreur s'est produite lors de la connexion", - "passwordForgot": "Mot de passe oublié ?", - "otpAuth": "Authentification à deux facteurs", - "otpAuthDescription": "Entrez le code de votre application d'authentification ou l'un de vos codes de secours à usage unique.", - "otpAuthSubmit": "Soumettre le code", - "idpContinue": "Ou continuer avec", - "otpAuthBack": "Retour à la connexion", - "navbar": "Menu de navigation", - "navbarDescription": "Menu de navigation principal de l'application", - "navbarDocsLink": "Documentation", - "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", - "otpSetupCheckCodeRetry": "Code invalide. Veuillez réessayer.", - "otpSetup": "Activer l'authentification à deux facteurs", - "otpSetupDescription": "Sécurisez votre compte avec une couche de protection supplémentaire", - "otpSetupScanQr": "Scannez ce code QR avec votre application d'authentification ou entrez la clé secrète manuellement :", - "otpSetupSecretCode": "Code d'authentification", - "otpSetupSuccess": "Authentification à deux facteurs activée", - "otpSetupSuccessStoreBackupCodes": "Votre compte est maintenant plus sécurisé. N'oubliez pas de sauvegarder vos codes de secours.", - "otpErrorDisable": "Impossible de désactiver l'A2F", - "otpErrorDisableDescription": "Une erreur s'est produite lors de la désactivation de l'A2F", - "otpRemove": "Désactiver l'authentification à deux facteurs", - "otpRemoveDescription": "Désactiver l'authentification à deux facteurs pour votre compte", - "otpRemoveSuccess": "Authentification à deux facteurs désactivée", - "otpRemoveSuccessMessage": "L'authentification à deux facteurs a été désactivée pour votre compte. Vous pouvez la réactiver à tout moment.", - "otpRemoveSubmit": "Désactiver l'A2F", - "paginator": "Page {current} sur {last}", - "paginatorToFirst": "Aller à la première page", - "paginatorToPrevious": "Aller à la page précédente", - "paginatorToNext": "Aller à la page suivante", - "paginatorToLast": "Aller à la dernière page", - "copyText": "Copier le texte", - "copyTextFailed": "Échec de la copie du texte : ", - "copyTextClipboard": "Copier dans le presse-papiers", - "inviteErrorInvalidConfirmation": "Confirmation invalide", - "passwordRequired": "Le mot de passe est requis", - "allowAll": "Tout autoriser", - "permissionsAllowAll": "Autoriser toutes les autorisations", - "githubUsernameRequired": "Le nom d'utilisateur GitHub est requis", - "supportKeyRequired": "La clé de supporter est requise", - "passwordRequirementsChars": "Le mot de passe doit comporter au moins 8 caractères", - "language": "Langue", - "verificationCodeRequired": "Le code est requis", - "userErrorNoUpdate": "Pas d'utilisateur à mettre à jour", - "siteErrorNoUpdate": "Pas de site à mettre à jour", - "resourceErrorNoUpdate": "Pas de ressource à mettre à jour", - "authErrorNoUpdate": "Pas d'informations d'authentification à mettre à jour", - "orgErrorNoUpdate": "Pas d'organisation à mettre à jour", - "orgErrorNoProvided": "Aucune organisation fournie", - "apiKeysErrorNoUpdate": "Pas de clé API à mettre à jour", - "sidebarOverview": "Aperçu", - "sidebarHome": "Domicile", - "sidebarSites": "Espaces", - "sidebarResources": "Ressource", - "sidebarAccessControl": "Contrôle d'accès", - "sidebarUsers": "Utilisateurs", - "sidebarInvitations": "Invitations", - "sidebarRoles": "Rôles", - "sidebarShareableLinks": "Liens partagables", - "sidebarApiKeys": "Clés API", - "sidebarSettings": "Réglages", - "sidebarAllUsers": "Tous les utilisateurs", - "sidebarIdentityProviders": "Fournisseurs d'identité", - "sidebarLicense": "Licence", - "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.", - "enableDockerSocketLink": "En savoir plus", - "viewDockerContainers": "Voir les conteneurs Docker", - "containersIn": "Conteneurs en {siteName}", - "selectContainerDescription": "Sélectionnez n'importe quel conteneur à utiliser comme nom d'hôte pour cette cible. Cliquez sur un port pour utiliser un port.", - "containerName": "Nom", - "containerImage": "Image", - "containerState": "État", - "containerNetworks": "Réseaux", - "containerHostnameIp": "Nom d'hôte/IP", - "containerLabels": "Étiquettes", - "containerLabelsCount": "{count, plural, one {# étiquette} other {# étiquettes}}", - "containerLabelsTitle": "Étiquettes de conteneur", - "containerLabelEmpty": "", - "containerPorts": "Ports", - "containerPortsMore": "+{count} de plus", - "containerActions": "Actions", - "select": "Sélectionner", - "noContainersMatchingFilters": "Aucun conteneur ne correspond aux filtres actuels.", - "showContainersWithoutPorts": "Afficher les conteneurs sans ports", - "showStoppedContainers": "Afficher les conteneurs arrêtés", - "noContainersFound": "Aucun conteneur trouvé. Assurez-vous que les conteneurs Docker sont en cours d'exécution.", - "searchContainersPlaceholder": "Rechercher dans les conteneurs {count}...", - "searchResultsCount": "{count, plural, one {# résultat} other {# résultats}}", - "filters": "Filtres", - "filterOptions": "Options de filtre", - "filterPorts": "Ports", - "filterStopped": "Arrêté", - "clearAllFilters": "Effacer tous les filtres", - "columns": "Colonnes", - "toggleColumns": "Activer/désactiver les colonnes", - "refreshContainersList": "Rafraîchir la liste des conteneurs", - "searching": "Recherche en cours...", - "noContainersFoundMatching": "Aucun conteneur correspondant à \"{filter}\".", - "light": "clair", - "dark": "sombre", - "system": "système", - "theme": "Thème", - "subnetRequired": "Le sous-réseau est requis", - "initialSetupTitle": "Configuration initiale du serveur", - "initialSetupDescription": "Créer le compte administrateur du serveur initial. Un seul administrateur serveur peut exister. Vous pouvez toujours changer ces informations d'identification plus tard.", - "createAdminAccount": "Créer un compte administrateur", - "setupErrorCreateAdmin": "Une erreur s'est produite lors de la création du compte administrateur du serveur.", - "certificateStatus": "Statut du certificat", - "loading": "Chargement", - "restart": "Redémarrer", - "domains": "Domaines", - "domainsDescription": "Gérer les domaines de votre organisation", - "domainsSearch": "Rechercher des domaines...", - "domainAdd": "Ajouter un domaine", - "domainAddDescription": "Enregistrez un nouveau domaine avec votre organisation", - "domainCreate": "Créer un domaine", - "domainCreatedDescription": "Domaine créé avec succès", - "domainDeletedDescription": "Domaine supprimé avec succès", - "domainQuestionRemove": "Êtes-vous sûr de vouloir supprimer le domaine {domain} de votre compte ?", - "domainMessageRemove": "Une fois supprimé, le domaine ne sera plus associé à votre compte.", - "domainMessageConfirm": "Pour confirmer, veuillez taper le nom du domaine ci-dessous.", - "domainConfirmDelete": "Confirmer la suppression du domaine", - "domainDelete": "Supprimer le domaine", - "domain": "Domaine", - "selectDomainTypeNsName": "Délégation de domaine (NS)", - "selectDomainTypeNsDescription": "Ce domaine et tous ses sous-domaines. Utilisez cela lorsque vous souhaitez contrôler une zone de domaine entière.", - "selectDomainTypeCnameName": "Domaine unique (CNAME)", - "selectDomainTypeCnameDescription": "Juste ce domaine spécifique. Utilisez ce paramètre pour des sous-domaines individuels ou des entrées de domaine spécifiques.", - "selectDomainTypeWildcardName": "Domaine Générique", - "selectDomainTypeWildcardDescription": "Ce domaine et ses sous-domaines.", - "domainDelegation": "Domaine Unique", - "selectType": "Sélectionnez un type", - "actions": "Actions", - "refresh": "Actualiser", - "refreshError": "Échec de l'actualisation des données", - "verified": "Vérifié", - "pending": "En attente", - "sidebarBilling": "Facturation", - "billing": "Facturation", - "orgBillingDescription": "Gérez vos informations de facturation et vos abonnements", - "github": "GitHub", - "pangolinHosted": "Pangolin Hébergement", - "fossorial": "Fossorial", - "completeAccountSetup": "Complétez la configuration du compte", - "completeAccountSetupDescription": "Définissez votre mot de passe pour commencer", - "accountSetupSent": "Nous enverrons un code de configuration de compte à cette adresse e-mail.", - "accountSetupCode": "Code de configuration", - "accountSetupCodeDescription": "Vérifiez votre e-mail pour le code de configuration.", - "passwordCreate": "Créer un mot de passe", - "passwordCreateConfirm": "Confirmer le mot de passe", - "accountSetupSubmit": "Envoyer le code de configuration", - "completeSetup": "Configuration complète", - "accountSetupSuccess": "Configuration du compte terminée! Bienvenue chez Pangolin !", - "documentation": "Documentation", - "saveAllSettings": "Enregistrer tous les paramètres", - "settingsUpdated": "Paramètres mis à jour", - "settingsUpdatedDescription": "Tous les paramètres ont été mis à jour avec succès", - "settingsErrorUpdate": "Échec de la mise à jour des paramètres", - "settingsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres", - "sidebarCollapse": "Réduire", - "sidebarExpand": "Développer", - "newtUpdateAvailable": "Mise à jour disponible", - "newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.", - "domainPickerEnterDomain": "Domaine", - "domainPickerPlaceholder": "monapp.exemple.com", - "domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.", - "domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles", - "domainPickerTabAll": "Tous", - "domainPickerTabOrganization": "Organisation", - "domainPickerTabProvided": "Fournis", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Vérification de la disponibilité...", - "domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé. Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.", - "domainPickerOrganizationDomains": "Domaines de l'organisation", - "domainPickerProvidedDomains": "Domaines fournis", - "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", - "createNewOrgDescription": "Créer une nouvelle organisation", - "organization": "Organisation", - "port": "Port", - "securityKeyManage": "Gérer les clés de sécurité", - "securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe", - "securityKeyRegister": "Enregistrer une nouvelle clé de sécurité", - "securityKeyList": "Vos clés de sécurité", - "securityKeyNone": "Aucune clé de sécurité enregistrée", - "securityKeyNameRequired": "Le nom est requis", - "securityKeyRemove": "Supprimer", - "securityKeyLastUsed": "Dernière utilisation : {date}", - "securityKeyNameLabel": "Nom", - "securityKeyRegisterSuccess": "Clé de sécurité enregistrée avec succès", - "securityKeyRegisterError": "Échec de l'enregistrement de la clé de sécurité", - "securityKeyRemoveSuccess": "Clé de sécurité supprimée avec succès", - "securityKeyRemoveError": "Échec de la suppression de la clé de sécurité", - "securityKeyLoadError": "Échec du chargement des clés de sécurité", - "securityKeyLogin": "Continuer avec une clé de sécurité", - "securityKeyAuthError": "Échec de l'authentification avec la clé de sécurité", - "securityKeyRecommendation": "Envisagez d'enregistrer une autre clé de sécurité sur un appareil différent pour vous assurer de ne pas être bloqué de votre compte.", - "registering": "Enregistrement...", - "securityKeyPrompt": "Veuillez vérifier votre identité à l'aide de votre clé de sécurité. Assurez-vous que votre clé de sécurité est connectée et prête.", - "securityKeyBrowserNotSupported": "Votre navigateur ne prend pas en charge les clés de sécurité. Veuillez utiliser un navigateur moderne comme Chrome, Firefox ou Safari.", - "securityKeyPermissionDenied": "Veuillez autoriser l'accès à votre clé de sécurité pour continuer la connexion.", - "securityKeyRemovedTooQuickly": "Veuillez garder votre clé de sécurité connectée jusqu'à ce que le processus de connexion soit terminé.", - "securityKeyNotSupported": "Votre clé de sécurité peut ne pas être compatible. Veuillez essayer une clé de sécurité différente.", - "securityKeyUnknownError": "Un problème est survenu avec votre clé de sécurité. Veuillez réessayer.", - "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.", - "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", - "securityKeyTwoFactorRequired": "Authentification à deux facteurs requise", - "securityKeyTwoFactorDescription": "Veuillez entrer votre code d'authentification à deux facteurs pour enregistrer la clé de sécurité", - "securityKeyTwoFactorRemoveDescription": "Veuillez entrer votre code d'authentification à deux facteurs pour supprimer la clé de sécurité", - "securityKeyTwoFactorCode": "Code à deux facteurs", - "securityKeyRemoveTitle": "Supprimer la clé de sécurité", - "securityKeyRemoveDescription": "Saisissez votre mot de passe pour supprimer la clé de sécurité \"{name}\"", - "securityKeyNoKeysRegistered": "Aucune clé de sécurité enregistrée", - "securityKeyNoKeysDescription": "Ajoutez une clé de sécurité pour améliorer la sécurité de votre compte", - "createDomainRequired": "Le domaine est requis", - "createDomainAddDnsRecords": "Ajouter des enregistrements DNS", - "createDomainAddDnsRecordsDescription": "Ajouter les enregistrements DNS suivants à votre fournisseur de domaine pour compléter la configuration.", - "createDomainNsRecords": "Enregistrements NS", - "createDomainRecord": "Enregistrement", - "createDomainType": "Type :", - "createDomainName": "Nom :", - "createDomainValue": "Valeur :", - "createDomainCnameRecords": "Enregistrements CNAME", - "createDomainARecords": "Enregistrements A", - "createDomainRecordNumber": "Enregistrement {number}", - "createDomainTxtRecords": "Enregistrements TXT", - "createDomainSaveTheseRecords": "Enregistrez ces enregistrements", - "createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.", - "createDomainDnsPropagation": "Propagation DNS", - "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", - "and": "et", - "privacyPolicy": "la politique de confidentialité" - }, - "siteRequired": "Le site est requis.", - "olmTunnel": "Tunnel Olm", - "olmTunnelDescription": "Utilisez Olm pour la connectivité client", - "errorCreatingClient": "Erreur lors de la création du client", - "clientDefaultsNotFound": "Les paramètres par défaut du client sont introuvables", - "createClient": "Créer un client", - "createClientDescription": "Créez un nouveau client pour vous connecter à vos sites", - "seeAllClients": "Voir tous les clients", - "clientInformation": "Informations client", - "clientNamePlaceholder": "Nom du client", - "address": "Adresse", - "subnetPlaceholder": "Sous-réseau", - "addressDescription": "L'adresse que ce client utilisera pour la connectivité", - "selectSites": "Sélectionner des sites", - "sitesDescription": "Le client aura une connectivité vers les sites sélectionnés", - "clientInstallOlm": "Installer Olm", - "clientInstallOlmDescription": "Faites fonctionner Olm sur votre système", - "clientOlmCredentials": "Identifiants Olm", - "clientOlmCredentialsDescription": "C'est ainsi qu'Olm s'authentifiera auprès du serveur", - "olmEndpoint": "Point de terminaison Olm", - "olmId": "ID Olm", - "olmSecretKey": "Clé secrète Olm", - "clientCredentialsSave": "Enregistrez vos identifiants", - "clientCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.", - "generalSettingsDescription": "Configurez les paramètres généraux pour ce client", - "clientUpdated": "Client mis à jour", - "clientUpdatedDescription": "Le client a été mis à jour.", - "clientUpdateFailed": "Échec de la mise à jour du client", - "clientUpdateError": "Une erreur s'est produite lors de la mise à jour du client.", - "sitesFetchFailed": "Échec de la récupération des sites", - "sitesFetchError": "Une erreur s'est produite lors de la récupération des sites.", - "olmErrorFetchReleases": "Une erreur s'est produite lors de la récupération des versions d'Olm.", - "olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.", - "remoteSubnets": "Sous-réseaux distants", - "enterCidrRange": "Entrez la plage CIDR", - "remoteSubnetsDescription": "Ajoutez des plages CIDR accessibles à distance depuis ce site à l'aide de clients. Utilisez le format comme 10.0.0.0/24. Cela s'applique UNIQUEMENT à la connectivité des clients VPN.", - "resourceEnableProxy": "Activer le proxy public", - "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", - "externalProxyEnabled": "Proxy externe activé", - "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", - "domainPickerBaseDomainLabel": "Domaine de base", - "domainPickerSearchDomains": "Rechercher des domaines...", - "domainPickerNoDomainsFound": "Aucun domaine trouvé", - "domainPickerLoadingDomains": "Chargement des domaines...", - "domainPickerSelectBaseDomain": "Sélectionnez le domaine de base...", - "domainPickerNotAvailableForCname": "Non disponible pour les domaines CNAME", - "domainPickerEnterSubdomainOrLeaveBlank": "Entrez un sous-domaine ou laissez vide pour utiliser le domaine de base.", - "domainPickerEnterSubdomainToSearch": "Entrez un sous-domaine pour rechercher et sélectionner parmi les domaines gratuits disponibles.", - "domainPickerFreeDomains": "Domaines gratuits", - "domainPickerSearchForAvailableDomains": "Rechercher des domaines disponibles", - "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", - "proxyPort": "Port", - "resourcesTableProxyResources": "Ressources proxy", - "resourcesTableClientResources": "Ressources client", - "resourcesTableNoProxyResourcesFound": "Aucune ressource proxy trouvée.", - "resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.", - "resourcesTableDestination": "Destination", - "resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec", - "resourcesTableClients": "Clients", - "resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.", - "editInternalResourceDialogEditClientResource": "Modifier la ressource client", - "editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.", - "editInternalResourceDialogResourceProperties": "Propriétés de la ressource", - "editInternalResourceDialogName": "Nom", - "editInternalResourceDialogProtocol": "Protocole", - "editInternalResourceDialogSitePort": "Port du site", - "editInternalResourceDialogTargetConfiguration": "Configuration de la cible", - "editInternalResourceDialogCancel": "Abandonner", - "editInternalResourceDialogSaveResource": "Enregistrer la ressource", - "editInternalResourceDialogSuccess": "Succès", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Ressource interne mise à jour avec succès", - "editInternalResourceDialogError": "Erreur", - "editInternalResourceDialogFailedToUpdateInternalResource": "Échec de la mise à jour de la ressource interne", - "editInternalResourceDialogNameRequired": "Le nom est requis", - "editInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères", - "editInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1", - "editInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide", - "editInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1", - "editInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536", - "createInternalResourceDialogNoSitesAvailable": "Aucun site disponible", - "createInternalResourceDialogNoSitesAvailableDescription": "Vous devez avoir au moins un site Newt avec un sous-réseau configuré pour créer des ressources internes.", - "createInternalResourceDialogClose": "Fermer", - "createInternalResourceDialogCreateClientResource": "Créer une ressource client", - "createInternalResourceDialogCreateClientResourceDescription": "Créez une ressource accessible aux clients connectés au site sélectionné.", - "createInternalResourceDialogResourceProperties": "Propriétés de la ressource", - "createInternalResourceDialogName": "Nom", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Sélectionner un site...", - "createInternalResourceDialogSearchSites": "Rechercher des sites...", - "createInternalResourceDialogNoSitesFound": "Aucun site trouvé.", - "createInternalResourceDialogProtocol": "Protocole", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Port du site", - "createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.", - "createInternalResourceDialogTargetConfiguration": "Configuration de la cible", - "createInternalResourceDialogDestinationIPDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.", - "createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.", - "createInternalResourceDialogCancel": "Abandonner", - "createInternalResourceDialogCreateResource": "Créer une ressource", - "createInternalResourceDialogSuccess": "Succès", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Ressource interne créée avec succès", - "createInternalResourceDialogError": "Erreur", - "createInternalResourceDialogFailedToCreateInternalResource": "Échec de la création de la ressource interne", - "createInternalResourceDialogNameRequired": "Le nom est requis", - "createInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères", - "createInternalResourceDialogPleaseSelectSite": "Veuillez sélectionner un site", - "createInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1", - "createInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide", - "createInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1", - "createInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536", - "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accepter les connexions client", - "siteAcceptClientConnectionsDescription": "Permet à d'autres appareils de se connecter via cette instance de Newt en tant que passerelle utilisant des clients.", - "siteAddress": "Adresse du site", - "siteAddressDescription": "Spécifiez l'adresse IP de l'hôte pour que les clients puissent s'y connecter. C'est l'adresse interne du site dans le réseau Pangolin pour que les clients puissent s'adresser. Doit être dans le sous-réseau de l'organisation.", - "autoLoginExternalIdp": "Connexion automatique avec IDP externe", - "autoLoginExternalIdpDescription": "Rediriger immédiatement l'utilisateur vers l'IDP externe pour l'authentification.", - "selectIdp": "Sélectionner l'IDP", - "selectIdpPlaceholder": "Choisissez un IDP...", - "selectIdpRequired": "Veuillez sélectionner un IDP lorsque la connexion automatique est activée.", - "autoLoginTitle": "Redirection", - "autoLoginDescription": "Redirection vers le fournisseur d'identité externe pour l'authentification.", - "autoLoginProcessing": "Préparation de l'authentification...", - "autoLoginRedirecting": "Redirection vers la connexion...", - "autoLoginError": "Erreur de connexion automatique", - "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", - "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.", - "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", - "introTitle": "Pangolin auto-hébergé géré", - "introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.", - "introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin — vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :", - "benefitSimplerOperations": { - "title": "Opérations plus simples", - "description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite." - }, - "benefitAutomaticUpdates": { - "title": "Mises à jour automatiques", - "description": "Le tableau de bord du cloud évolue rapidement, de sorte que vous obtenez de nouvelles fonctionnalités et des corrections de bugs sans avoir à extraire manuellement de nouveaux conteneurs à chaque fois." - }, - "benefitLessMaintenance": { - "title": "Moins de maintenance", - "description": "Aucune migration de base de données, sauvegarde ou infrastructure supplémentaire à gérer. Nous gérons cela dans le cloud." - }, - "benefitCloudFailover": { - "title": "Basculement du Cloud", - "description": "Si votre nœud descend, vos tunnels peuvent temporairement échouer jusqu'à ce que vous le rapatriez en ligne." - }, - "benefitHighAvailability": { - "title": "Haute disponibilité (PoPs)", - "description": "Vous pouvez également attacher plusieurs nœuds à votre compte pour une redondance et de meilleures performances." - }, - "benefitFutureEnhancements": { - "title": "Améliorations futures", - "description": "Nous prévoyons d'ajouter plus d'outils d'analyse, d'alerte et de gestion pour rendre votre déploiement encore plus robuste." - }, - "docsAlert": { - "text": "En savoir plus sur l'option Auto-Hébergement géré dans notre", - "documentation": "documentation" - }, - "convertButton": "Convertir ce noeud en auto-hébergé géré" - }, - "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", - "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é", - "domainPickerUnverified": "Non vérifié", - "domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.", - "domainPickerError": "Erreur", - "domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation", - "domainPickerErrorCheckAvailability": "Impossible de vérifier la disponibilité du domaine", - "domainPickerInvalidSubdomain": "Sous-domaine invalide", - "domainPickerInvalidSubdomainRemoved": "L'entrée \"{sub}\" a été supprimée car elle n'est pas valide.", - "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", - "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 deleted file mode 100644 index 8031f60e..00000000 --- a/messages/it-IT.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Crea la tua organizzazione, sito e risorse", - "setupNewOrg": "Nuova Organizzazione", - "setupCreateOrg": "Crea Organizzazione", - "setupCreateResources": "Crea Risorse", - "setupOrgName": "Nome Dell'Organizzazione", - "orgDisplayName": "Questo è il nome visualizzato della tua organizzazione.", - "orgId": "Id Organizzazione", - "setupIdentifierMessage": "Questo è l' identificatore univoco della tua organizzazione. Questo è separato dal nome del display.", - "setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.", - "componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.", - "componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.", - "welcome": "Benvenuti a Pangolin", - "welcomeTo": "Benvenuto a", - "componentsCreateOrg": "Crea un'organizzazione", - "componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.", - "componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.", - "dismiss": "Ignora", - "componentsLicenseViolation": "Violazione della licenza: Questo server sta usando i siti {usedSites} che superano il suo limite concesso in licenza per i siti {maxSites} . Segui i termini di licenza per continuare a usare tutte le funzionalità.", - "componentsSupporterMessage": "Grazie per aver supportato Pangolin come {tier}!", - "inviteErrorNotValid": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia stato accettato o non sia più valido.", - "inviteErrorUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per questo utente.", - "inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.", - "inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.", - "inviteCreateUser": "Si prega di creare un account prima.", - "goHome": "Vai A Home", - "inviteLogInOtherUser": "Accedi come utente diverso", - "createAnAccount": "Crea un account", - "inviteNotAccepted": "Invito Non Accettato", - "authCreateAccount": "Crea un account per iniziare", - "authNoAccount": "Non hai un account?", - "email": "Email", - "password": "Password", - "confirmPassword": "Conferma Password", - "createAccount": "Crea Account", - "viewSettings": "Visualizza impostazioni", - "delete": "Elimina", - "name": "Nome", - "online": "In linea", - "offline": "Non in linea", - "site": "Sito", - "dataIn": "Dati In", - "dataOut": "Dati Fuori", - "connectionType": "Tipo Di Connessione", - "tunnelType": "Tipo Di Tunnel", - "local": "Locale", - "edit": "Modifica", - "siteConfirmDelete": "Conferma Eliminazione Sito", - "siteDelete": "Elimina Sito", - "siteMessageRemove": "Una volta rimosso, il sito non sarà più accessibile. Anche tutte le risorse e gli obiettivi associati al sito saranno rimossi.", - "siteMessageConfirm": "Per confermare, digita il nome del sito qui sotto.", - "siteQuestionRemove": "Sei sicuro di voler rimuovere il sito {selectedSite} dall'organizzazione?", - "siteManageSites": "Gestisci Siti", - "siteDescription": "Consenti la connettività alla rete attraverso tunnel sicuri", - "siteCreate": "Crea Sito", - "siteCreateDescription2": "Segui i passaggi qui sotto per creare e collegare un nuovo sito", - "siteCreateDescription": "Crea un nuovo sito per iniziare a connettere le tue risorse", - "close": "Chiudi", - "siteErrorCreate": "Errore nella creazione del sito", - "siteErrorCreateKeyPair": "Coppia di chiavi o valori predefiniti del sito non trovati", - "siteErrorCreateDefaults": "Predefiniti del sito non trovati", - "method": "Metodo", - "siteMethodDescription": "Questo è il modo in cui esporrete le connessioni.", - "siteLearnNewt": "Scopri come installare Newt sul tuo sistema", - "siteSeeConfigOnce": "Potrai vedere la configurazione solo una volta.", - "siteLoadWGConfig": "Caricamento configurazione WireGuard...", - "siteDocker": "Espandi per i dettagli di distribuzione Docker", - "toggle": "Attiva/disattiva", - "dockerCompose": "Composizione Docker", - "dockerRun": "Corsa Docker", - "siteLearnLocal": "I siti locali non tunnel, saperne di più", - "siteConfirmCopy": "Ho copiato la configurazione", - "searchSitesProgress": "Cerca siti...", - "siteAdd": "Aggiungi Sito", - "siteInstallNewt": "Installa Newt", - "siteInstallNewtDescription": "Esegui Newt sul tuo sistema", - "WgConfiguration": "Configurazione WireGuard", - "WgConfigurationDescription": "Usa la seguente configurazione per connetterti alla tua rete", - "operatingSystem": "Sistema Operativo", - "commands": "Comandi", - "recommended": "Consigliato", - "siteNewtDescription": "Per la migliore esperienza utente, utilizzare Newt. Utilizza WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.", - "siteRunsInDocker": "Esegue nel Docker", - "siteRunsInShell": "Esegue in shell su macOS, Linux e Windows", - "siteErrorDelete": "Errore nell'eliminare il sito", - "siteErrorUpdate": "Impossibile aggiornare il sito", - "siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.", - "siteUpdated": "Sito aggiornato", - "siteUpdatedDescription": "Il sito è stato aggiornato.", - "siteGeneralDescription": "Configura le impostazioni generali per questo sito", - "siteSettingDescription": "Configura le impostazioni sul tuo sito", - "siteSetting": "Impostazioni {siteName}", - "siteNewtTunnel": "Tunnel Newt (Consigliato)", - "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.", - "siteWg": "WireGuard Base", - "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", - "siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI", - "siteLocalDescription": "Solo risorse locali. Nessun tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", - "siteSeeAll": "Vedi Tutti I Siti", - "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", - "siteNewtCredentials": "Credenziali Newt", - "siteNewtCredentialsDescription": "Questo è come Newt si autenticerà con il server", - "siteCredentialsSave": "Salva Le Tue Credenziali", - "siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", - "siteInfo": "Informazioni Sito", - "status": "Stato", - "shareTitle": "Gestisci Collegamenti Di Condivisione", - "shareDescription": "Crea link condivisibili per concedere un accesso temporaneo o permanente alle tue risorse", - "shareSearch": "Cerca link condivisi...", - "shareCreate": "Crea Link Di Condivisione", - "shareErrorDelete": "Impossibile eliminare il link", - "shareErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione del link", - "shareDeleted": "Link eliminato", - "shareDeletedDescription": "Il link è stato eliminato", - "shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.", - "accessToken": "Token Di Accesso", - "usageExamples": "Esempi Di Utilizzo", - "tokenId": "ID del Token", - "requestHeades": "Richiedi Intestazioni", - "queryParameter": "Parametro Query", - "importantNote": "Nota Importante", - "shareImportantDescription": "Per motivi di sicurezza, si consiglia di utilizzare le intestazioni su parametri di query quando possibile, in quanto i parametri di query possono essere registrati in log server o cronologia browser.", - "token": "Token", - "shareTokenSecurety": "Mantieni sicuro il tuo token di accesso. Non condividerlo in aree accessibili al pubblico o codice lato client.", - "shareErrorFetchResource": "Recupero delle risorse non riuscito", - "shareErrorFetchResourceDescription": "Si è verificato un errore durante il recupero delle risorse", - "shareErrorCreate": "Impossibile creare il link di condivisione", - "shareErrorCreateDescription": "Si è verificato un errore durante la creazione del link di condivisione", - "shareCreateDescription": "Chiunque con questo link può accedere alla risorsa", - "shareTitleOptional": "Titolo (facoltativo)", - "expireIn": "Scadenza In", - "neverExpire": "Mai scadere", - "shareExpireDescription": "Il tempo di scadenza è per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.", - "shareSeeOnce": "Potrai vedere solo questo linkonce. Assicurati di copiarlo.", - "shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.", - "shareTokenUsage": "Vedi Utilizzo Token Di Accesso", - "createLink": "Crea Collegamento", - "resourcesNotFound": "Nessuna risorsa trovata", - "resourceSearch": "Cerca risorse", - "openMenu": "Apri menu", - "resource": "Risorsa", - "title": "Titolo", - "created": "Creato", - "expires": "Scade", - "never": "Mai", - "shareErrorSelectResource": "Seleziona una risorsa", - "resourceTitle": "Gestisci Risorse", - "resourceDescription": "Crea proxy sicuri per le tue applicazioni private", - "resourcesSearch": "Cerca risorse...", - "resourceAdd": "Aggiungi Risorsa", - "resourceErrorDelte": "Errore nell'eliminare la risorsa", - "authentication": "Autenticazione", - "protected": "Protetto", - "notProtected": "Non Protetto", - "resourceMessageRemove": "Una volta rimossa, la risorsa non sarà più accessibile. Tutti gli obiettivi associati alla risorsa saranno rimossi.", - "resourceMessageConfirm": "Per confermare, digita il nome della risorsa qui sotto.", - "resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa {selectedResource} dall'organizzazione?", - "resourceHTTP": "Risorsa HTTPS", - "resourceHTTPDescription": "Richieste proxy alla tua app tramite HTTPS utilizzando un sottodominio o un dominio di base.", - "resourceRaw": "Risorsa Raw TCP/UDP", - "resourceRawDescription": "Richieste proxy alla tua app tramite TCP/UDP utilizzando un numero di porta.", - "resourceCreate": "Crea Risorsa", - "resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa", - "resourceSeeAll": "Vedi Tutte Le Risorse", - "resourceInfo": "Informazioni Risorsa", - "resourceNameDescription": "Questo è il nome visualizzato per la risorsa.", - "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", - "resourceHTTPSSettings": "Impostazioni HTTPS", - "resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla tua risorsa su HTTPS", - "domainType": "Tipo Di Dominio", - "subdomain": "Sottodominio", - "baseDomain": "Dominio Base", - "subdomnainDescription": "Il sottodominio in cui la tua risorsa sarà accessibile.", - "resourceRawSettings": "Impostazioni TCP/UDP", - "resourceRawSettingsDescription": "Configura come accedere alla tua risorsa tramite TCP/UDP", - "protocol": "Protocollo", - "protocolSelect": "Seleziona un protocollo", - "resourcePortNumber": "Numero Porta", - "resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.", - "cancel": "Annulla", - "resourceConfig": "Snippet Di Configurazione", - "resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la tua risorsa TCP/UDP", - "resourceAddEntrypoints": "Traefik: Aggiungi Ingresso", - "resourceExposePorts": "Gerbil: espone le porte in Docker componi", - "resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP", - "resourceBack": "Torna alle risorse", - "resourceGoTo": "Vai alla Risorsa", - "resourceDelete": "Elimina Risorsa", - "resourceDeleteConfirm": "Conferma Eliminazione Risorsa", - "visibility": "Visibilità", - "enabled": "Abilitato", - "disabled": "Disabilitato", - "general": "Generale", - "generalSettings": "Impostazioni Generali", - "proxy": "Proxy", - "internal": "Interno", - "rules": "Regole", - "resourceSettingDescription": "Configura le impostazioni sulla tua risorsa", - "resourceSetting": "Impostazioni {resourceName}", - "alwaysAllow": "Consenti Sempre", - "alwaysDeny": "Nega Sempre", - "passToAuth": "Passa all'autenticazione", - "orgSettingsDescription": "Configura le impostazioni generali della tua organizzazione", - "orgGeneralSettings": "Impostazioni Organizzazione", - "orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione", - "saveGeneralSettings": "Salva Impostazioni Generali", - "saveSettings": "Salva Impostazioni", - "orgDangerZone": "Zona Pericolosa", - "orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.", - "orgDelete": "Elimina Organizzazione", - "orgDeleteConfirm": "Conferma Elimina Organizzazione", - "orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.", - "orgMessageConfirm": "Per confermare, digita il nome dell'organizzazione qui sotto.", - "orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione {selectedOrg}?", - "orgUpdated": "Organizzazione aggiornata", - "orgUpdatedDescription": "L'organizzazione è stata aggiornata.", - "orgErrorUpdate": "Impossibile aggiornare l'organizzazione", - "orgErrorUpdateMessage": "Si è verificato un errore nell'aggiornamento dell'organizzazione.", - "orgErrorFetch": "Recupero delle organizzazioni non riuscito", - "orgErrorFetchMessage": "Si è verificato un errore durante l'elenco delle organizzazioni", - "orgErrorDelete": "Impossibile eliminare l'organizzazione", - "orgErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione dell'organizzazione.", - "orgDeleted": "Organizzazione eliminata", - "orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.", - "orgMissing": "ID Organizzazione Mancante", - "orgMissingMessage": "Impossibile rigenerare l'invito senza un ID organizzazione.", - "accessUsersManage": "Gestisci Utenti", - "accessUsersDescription": "Invita gli utenti e aggiungili ai ruoli per gestire l'accesso alla tua organizzazione", - "accessUsersSearch": "Cerca utenti...", - "accessUserCreate": "Crea Utente", - "accessUserRemove": "Rimuovi Utente", - "username": "Nome utente", - "identityProvider": "Provider Di Identità", - "role": "Ruolo", - "nameRequired": "Il nome è obbligatorio", - "accessRolesManage": "Gestisci Ruoli", - "accessRolesDescription": "Configura i ruoli per gestire l'accesso alla tua organizzazione", - "accessRolesSearch": "Ricerca ruoli...", - "accessRolesAdd": "Aggiungi Ruolo", - "accessRoleDelete": "Elimina Ruolo", - "description": "Descrizione", - "inviteTitle": "Inviti Aperti", - "inviteDescription": "Gestisci i tuoi inviti ad altri utenti", - "inviteSearch": "Cerca inviti...", - "minutes": "Minuti", - "hours": "Ore", - "days": "Giorni", - "weeks": "Settimane", - "months": "Mesi", - "years": "Anni", - "day": "{count, plural, one {# giorno} other {# giorni}}", - "apiKeysTitle": "Informazioni Chiave API", - "apiKeysConfirmCopy2": "Devi confermare di aver copiato la chiave API.", - "apiKeysErrorCreate": "Errore nella creazione della chiave API", - "apiKeysErrorSetPermission": "Errore nell'impostazione dei permessi", - "apiKeysCreate": "Genera Chiave API", - "apiKeysCreateDescription": "Genera una nuova chiave API per la tua organizzazione", - "apiKeysGeneralSettings": "Permessi", - "apiKeysGeneralSettingsDescription": "Determina cosa può fare questa chiave API", - "apiKeysList": "La Tua Chiave API", - "apiKeysSave": "Salva La Tua Chiave API", - "apiKeysSaveDescription": "Potrai vederla solo una volta. Assicurati di copiarla in un luogo sicuro.", - "apiKeysInfo": "La tua chiave API è:", - "apiKeysConfirmCopy": "Ho copiato la chiave API", - "generate": "Genera", - "done": "Fatto", - "apiKeysSeeAll": "Vedi Tutte Le Chiavi API", - "apiKeysPermissionsErrorLoadingActions": "Errore nel caricamento delle azioni della chiave API", - "apiKeysPermissionsErrorUpdate": "Errore nell'impostazione dei permessi", - "apiKeysPermissionsUpdated": "Permessi aggiornati", - "apiKeysPermissionsUpdatedDescription": "I permessi sono stati aggiornati.", - "apiKeysPermissionsGeneralSettings": "Permessi", - "apiKeysPermissionsGeneralSettingsDescription": "Determina cosa può fare questa chiave API", - "apiKeysPermissionsSave": "Salva Permessi", - "apiKeysPermissionsTitle": "Permessi", - "apiKeys": "Chiavi API", - "searchApiKeys": "Cerca chiavi API...", - "apiKeysAdd": "Genera Chiave API", - "apiKeysErrorDelete": "Errore nell'eliminazione della chiave API", - "apiKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave API", - "apiKeysQuestionRemove": "Sei sicuro di voler rimuovere la chiave API {selectedApiKey} dall'organizzazione?", - "apiKeysMessageRemove": "Una volta rimossa, la chiave API non potrà più essere utilizzata.", - "apiKeysMessageConfirm": "Per confermare, digita il nome della chiave API qui sotto.", - "apiKeysDeleteConfirm": "Conferma Eliminazione Chiave API", - "apiKeysDelete": "Elimina Chiave API", - "apiKeysManage": "Gestisci Chiavi API", - "apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione", - "apiKeysSettings": "Impostazioni {apiKeyName}", - "userTitle": "Gestisci Tutti Gli Utenti", - "userDescription": "Visualizza e gestisci tutti gli utenti del sistema", - "userAbount": "Informazioni Sulla Gestione Utente", - "userAbountDescription": "Questa tabella mostra tutti gli oggetti utente root nel sistema. Ogni utente può appartenere a più organizzazioni. La rimozione di un utente da un'organizzazione non elimina il suo oggetto utente root, che rimarrà nel sistema. Per rimuovere completamente un utente dal sistema, è necessario eliminare il loro oggetto utente root utilizzando l'azione di eliminazione in questa tabella.", - "userServer": "Utenti Server", - "userSearch": "Cerca utenti del server...", - "userErrorDelete": "Errore nell'eliminare l'utente", - "userDeleteConfirm": "Conferma Eliminazione Utente", - "userDeleteServer": "Elimina utente dal server", - "userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni ed essere completamente rimosso dal server.", - "userMessageConfirm": "Per confermare, digita il nome dell'utente qui sotto.", - "userQuestionRemove": "Sei sicuro di voler eliminare definitivamente {selectedUser} dal server?", - "licenseKey": "Chiave Di Licenza", - "valid": "Valido", - "numberOfSites": "Numero di siti", - "licenseKeySearch": "Cerca chiavi di licenza...", - "licenseKeyAdd": "Aggiungi Chiave Di Licenza", - "type": "Tipo", - "licenseKeyRequired": "La chiave di licenza è obbligatoria", - "licenseTermsAgree": "Devi accettare i termini della licenza", - "licenseErrorKeyLoad": "Impossibile caricare le chiavi di licenza", - "licenseErrorKeyLoadDescription": "Si è verificato un errore durante il caricamento delle chiavi di licenza.", - "licenseErrorKeyDelete": "Impossibile eliminare la chiave di licenza", - "licenseErrorKeyDeleteDescription": "Si è verificato un errore durante l'eliminazione della chiave di licenza.", - "licenseKeyDeleted": "Chiave di licenza eliminata", - "licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.", - "licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita", - "licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.", - "licenseAbout": "Informazioni Su Licenze", - "communityEdition": "Edizione Community", - "licenseAboutDescription": "Questo è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.", - "licenseKeyActivated": "Chiave di licenza attivata", - "licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.", - "licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza", - "licenseErrorKeyRecheckDescription": "Si è verificato un errore nel ricontrollare le chiavi di licenza.", - "licenseErrorKeyRechecked": "Chiavi di licenza ricontrollate", - "licenseErrorKeyRecheckedDescription": "Tutte le chiavi di licenza sono state ricontrollate", - "licenseActivateKey": "Attiva Chiave Di Licenza", - "licenseActivateKeyDescription": "Inserisci una chiave di licenza per attivarla.", - "licenseActivate": "Attiva Licenza", - "licenseAgreement": "Selezionando questa casella, confermi di aver letto e accettato i termini di licenza corrispondenti al livello associato alla chiave di licenza.", - "fossorialLicense": "Visualizza I Termini Di Licenza Commerciale Fossorial E Abbonamento", - "licenseMessageRemove": "Questo rimuoverà la chiave di licenza e tutti i permessi associati da essa concessi.", - "licenseMessageConfirm": "Per confermare, digitare la chiave di licenza qui sotto.", - "licenseQuestionRemove": "Sei sicuro di voler eliminare la chiave di licenza {selectedKey}?", - "licenseKeyDelete": "Elimina Chiave Di Licenza", - "licenseKeyDeleteConfirm": "Conferma Elimina Chiave Di Licenza", - "licenseTitle": "Gestisci Stato Licenza", - "licenseTitleDescription": "Visualizza e gestisci le chiavi di licenza nel sistema", - "licenseHost": "Licenza Host", - "licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.", - "licensedNot": "Non Licenziato", - "hostId": "ID Host", - "licenseReckeckAll": "Ricontrolla Tutte Le Tasti", - "licenseSiteUsage": "Utilizzo Siti", - "licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.", - "licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.", - "licensePurchase": "Acquista Licenza", - "licensePurchaseSites": "Acquista Siti Aggiuntivi", - "licenseSitesUsedMax": "{usedSites} di {maxSites} siti utilizzati", - "licenseSitesUsed": "{count, plural, =0 {# siti} one {# sito} other {# siti}} nel sistema.", - "licensePurchaseDescription": "Scegli quanti siti vuoi {selectedMode, select, license {acquista una licenza. Puoi sempre aggiungere altri siti più tardi.} other {aggiungi alla tua licenza esistente.}}", - "licenseFee": "Costo della licenza", - "licensePriceSite": "Prezzo per sito", - "total": "Totale", - "licenseContinuePayment": "Continua al pagamento", - "pricingPage": "pagina prezzi", - "pricingPortal": "Vedi Il Portale Di Acquisto", - "licensePricingPage": "Per i prezzi e gli sconti più aggiornati, visita il ", - "invite": "Inviti", - "inviteRegenerate": "Rigenera Invito", - "inviteRegenerateDescription": "Revoca l'invito precedente e creane uno nuovo", - "inviteRemove": "Rimuovi Invito", - "inviteRemoveError": "Impossibile rimuovere l'invito", - "inviteRemoveErrorDescription": "Si è verificato un errore durante la rimozione dell'invito.", - "inviteRemoved": "Invito rimosso", - "inviteRemovedDescription": "L'invito per {email} è stato rimosso.", - "inviteQuestionRemove": "Sei sicuro di voler rimuovere l'invito {email}?", - "inviteMessageRemove": "Una volta rimosso, questo invito non sarà più valido. Puoi sempre reinvitare l'utente in seguito.", - "inviteMessageConfirm": "Per confermare, digita l'indirizzo email dell'invito qui sotto.", - "inviteQuestionRegenerate": "Sei sicuro di voler rigenerare l'invito {email}? Questo revocherà l'invito precedente.", - "inviteRemoveConfirm": "Conferma Rimozione Invito", - "inviteRegenerated": "Invito Rigenerato", - "inviteSent": "Un nuovo invito è stato inviato a {email}.", - "inviteSentEmail": "Invia notifica email all'utente", - "inviteGenerate": "Un nuovo invito è stato generato per {email}.", - "inviteDuplicateError": "Invito Duplicato", - "inviteDuplicateErrorDescription": "Esiste già un invito per questo utente.", - "inviteRateLimitError": "Limite di Frequenza Superato", - "inviteRateLimitErrorDescription": "Hai superato il limite di 3 rigenerazioni per ora. Riprova più tardi.", - "inviteRegenerateError": "Impossibile Rigenerare l'Invito", - "inviteRegenerateErrorDescription": "Si è verificato un errore durante la rigenerazione dell'invito.", - "inviteValidityPeriod": "Periodo di Validità", - "inviteValidityPeriodSelect": "Seleziona periodo di validità", - "inviteRegenerateMessage": "L'invito è stato rigenerato. L'utente deve accedere al link qui sotto per accettare l'invito.", - "inviteRegenerateButton": "Rigenera", - "expiresAt": "Scade Il", - "accessRoleUnknown": "Ruolo Sconosciuto", - "placeholder": "Segnaposto", - "userErrorOrgRemove": "Impossibile rimuovere l'utente", - "userErrorOrgRemoveDescription": "Si è verificato un errore durante la rimozione dell'utente.", - "userOrgRemoved": "Utente rimosso", - "userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.", - "userQuestionOrgRemove": "Sei sicuro di voler rimuovere {email} dall'organizzazione?", - "userMessageOrgRemove": "Una volta rimosso, questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.", - "userMessageOrgConfirm": "Per confermare, digita il nome dell'utente qui sotto.", - "userRemoveOrgConfirm": "Conferma Rimozione Utente", - "userRemoveOrg": "Rimuovi Utente dall'Organizzazione", - "users": "Utenti", - "accessRoleMember": "Membro", - "accessRoleOwner": "Proprietario", - "userConfirmed": "Confermato", - "idpNameInternal": "Interno", - "emailInvalid": "Indirizzo email non valido", - "inviteValidityDuration": "Seleziona una durata", - "accessRoleSelectPlease": "Seleziona un ruolo", - "usernameRequired": "Username richiesto", - "idpSelectPlease": "Seleziona un provider di identità", - "idpGenericOidc": "Provider OAuth2/OIDC generico.", - "accessRoleErrorFetch": "Impossibile recuperare i ruoli", - "accessRoleErrorFetchDescription": "Si è verificato un errore durante il recupero dei ruoli", - "idpErrorFetch": "Impossibile recuperare i provider di identità", - "idpErrorFetchDescription": "Si è verificato un errore durante il recupero dei provider di identità", - "userErrorExists": "Utente Già Esistente", - "userErrorExistsDescription": "Questo utente è già membro dell'organizzazione.", - "inviteError": "Impossibile invitare l'utente", - "inviteErrorDescription": "Si è verificato un errore durante l'invito dell'utente", - "userInvited": "Utente invitato", - "userInvitedDescription": "L'utente è stato invitato con successo.", - "userErrorCreate": "Impossibile creare l'utente", - "userErrorCreateDescription": "Si è verificato un errore durante la creazione dell'utente", - "userCreated": "Utente creato", - "userCreatedDescription": "L'utente è stato creato con successo.", - "userTypeInternal": "Utente Interno", - "userTypeInternalDescription": "Invita un utente a unirsi direttamente alla tua organizzazione.", - "userTypeExternal": "Utente Esterno", - "userTypeExternalDescription": "Crea un utente con un provider di identità esterno.", - "accessUserCreateDescription": "Segui i passaggi seguenti per creare un nuovo utente", - "userSeeAll": "Vedi Tutti gli Utenti", - "userTypeTitle": "Tipo di Utente", - "userTypeDescription": "Determina come vuoi creare l'utente", - "userSettings": "Informazioni Utente", - "userSettingsDescription": "Inserisci i dettagli per il nuovo utente", - "inviteEmailSent": "Invia email di invito all'utente", - "inviteValid": "Valido Per", - "selectDuration": "Seleziona durata", - "accessRoleSelect": "Seleziona ruolo", - "inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.", - "inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.", - "inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.", - "idpTitle": "Informazioni Generali", - "idpSelect": "Seleziona il provider di identità per l'utente esterno", - "idpNotConfigured": "Nessun provider di identità configurato. Configura un provider di identità prima di creare utenti esterni.", - "usernameUniq": "Questo deve corrispondere all'username univoco esistente nel provider di identità selezionato.", - "emailOptional": "Email (Opzionale)", - "nameOptional": "Nome (Opzionale)", - "accessControls": "Controlli di Accesso", - "userDescription2": "Gestisci le impostazioni di questo utente", - "accessRoleErrorAdd": "Impossibile aggiungere l'utente al ruolo", - "accessRoleErrorAddDescription": "Si è verificato un errore durante l'aggiunta dell'utente al ruolo.", - "userSaved": "Utente salvato", - "userSavedDescription": "L'utente è stato aggiornato.", - "autoProvisioned": "Auto Provisioned", - "autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità", - "accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione", - "accessControlsSubmit": "Salva Controlli di Accesso", - "roles": "Ruoli", - "accessUsersRoles": "Gestisci Utenti e Ruoli", - "accessUsersRolesDescription": "Invita utenti e aggiungili ai ruoli per gestire l'accesso alla tua organizzazione", - "key": "Chiave", - "createdAt": "Creato Il", - "proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.", - "proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.", - "proxyEnableSSL": "Abilita SSL", - "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.", - "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", - "siteErrorFetchDescription": "Si è verificato un errore durante il recupero della risorsa", - "targetErrorDuplicate": "Target duplicato", - "targetErrorDuplicateDescription": "Esiste già un target con queste impostazioni", - "targetWireGuardErrorInvalidIp": "IP target non valido", - "targetWireGuardErrorInvalidIpDescription": "L'IP target deve essere all'interno della subnet del sito", - "targetsUpdated": "Target aggiornati", - "targetsUpdatedDescription": "Target e impostazioni aggiornati con successo", - "targetsErrorUpdate": "Impossibile aggiornare i target", - "targetsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento dei target", - "targetTlsUpdate": "Impostazioni TLS aggiornate", - "targetTlsUpdateDescription": "Le tue impostazioni TLS sono state aggiornate con successo", - "targetErrorTlsUpdate": "Impossibile aggiornare le impostazioni TLS", - "targetErrorTlsUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni TLS", - "proxyUpdated": "Impostazioni proxy aggiornate", - "proxyUpdatedDescription": "Le tue impostazioni proxy sono state aggiornate con successo", - "proxyErrorUpdate": "Impossibile aggiornare le impostazioni proxy", - "proxyErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni proxy", - "targetAddr": "IP / Nome host", - "targetPort": "Porta", - "targetProtocol": "Protocollo", - "targetTlsSettings": "Configurazione Connessione Sicura", - "targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa", - "targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate", - "targetTlsSni": "Nome Server Tls", - "targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.", - "targetTlsSubmit": "Salva Impostazioni", - "targets": "Configurazione Target", - "targetsDescription": "Configura i target per instradare il traffico ai tuoi servizi backend", - "targetStickySessions": "Abilita Sessioni Persistenti", - "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", - "methodSelect": "Seleziona metodo", - "targetSubmit": "Aggiungi Target", - "targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.", - "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", - "proxyCustomHeaderDescription": "L'intestazione host da impostare durante il proxy delle richieste. Lascia vuoto per usare quella predefinita.", - "proxyAdditionalSubmit": "Salva Impostazioni Proxy", - "subnetMaskErrorInvalid": "Maschera di sottorete non valida. Deve essere tra 0 e 32.", - "ipAddressErrorInvalidFormat": "Formato indirizzo IP non valido", - "ipAddressErrorInvalidOctet": "Ottetto indirizzo IP non valido", - "path": "Percorso", - "matchPath": "Corrispondenza Tracciato", - "ipAddressRange": "Intervallo IP", - "rulesErrorFetch": "Impossibile recuperare le regole", - "rulesErrorFetchDescription": "Si è verificato un errore durante il recupero delle regole", - "rulesErrorDuplicate": "Regola duplicata", - "rulesErrorDuplicateDescription": "Esiste già una regola con queste impostazioni", - "rulesErrorInvalidIpAddressRange": "CIDR non valido", - "rulesErrorInvalidIpAddressRangeDescription": "Inserisci un valore CIDR valido", - "rulesErrorInvalidUrl": "Percorso URL non valido", - "rulesErrorInvalidUrlDescription": "Inserisci un valore di percorso URL valido", - "rulesErrorInvalidIpAddress": "IP non valido", - "rulesErrorInvalidIpAddressDescription": "Inserisci un indirizzo IP valido", - "rulesErrorUpdate": "Impossibile aggiornare le regole", - "rulesErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle regole", - "rulesUpdated": "Abilita Regole", - "rulesUpdatedDescription": "La valutazione delle regole è stata aggiornata", - "rulesMatchIpAddressRangeDescription": "Inserisci un indirizzo in formato CIDR (es. 103.21.244.0/22)", - "rulesMatchIpAddress": "Inserisci un indirizzo IP (es. 103.21.244.12)", - "rulesMatchUrl": "Inserisci un percorso URL o pattern (es. /api/v1/todos o /api/v1/*)", - "rulesErrorInvalidPriority": "Priorità Non Valida", - "rulesErrorInvalidPriorityDescription": "Inserisci una priorità valida", - "rulesErrorDuplicatePriority": "Priorità Duplicate", - "rulesErrorDuplicatePriorityDescription": "Inserisci priorità uniche", - "ruleUpdated": "Regole aggiornate", - "ruleUpdatedDescription": "Regole aggiornate con successo", - "ruleErrorUpdate": "Operazione fallita", - "ruleErrorUpdateDescription": "Si è verificato un errore durante il salvataggio", - "rulesPriority": "Priorità", - "rulesAction": "Azione", - "rulesMatchType": "Tipo di Corrispondenza", - "value": "Valore", - "rulesAbout": "Informazioni sulle Regole", - "rulesAboutDescription": "Le regole ti permettono di controllare l'accesso alla tua risorsa in base a una serie di criteri. Puoi creare regole per consentire o negare l'accesso basato su indirizzo IP o percorso URL.", - "rulesActions": "Azioni", - "rulesActionAlwaysAllow": "Consenti Sempre: Ignora tutti i metodi di autenticazione", - "rulesActionAlwaysDeny": "Nega Sempre: Blocca tutte le richieste; nessuna autenticazione può essere tentata", - "rulesActionPassToAuth": "Passa all'autenticazione: Consenti di tentare i metodi di autenticazione", - "rulesMatchCriteria": "Criteri di Corrispondenza", - "rulesMatchCriteriaIpAddress": "Corrisponde a un indirizzo IP specifico", - "rulesMatchCriteriaIpAddressRange": "Corrisponde a un intervallo di indirizzi IP in notazione CIDR", - "rulesMatchCriteriaUrl": "Corrisponde a un percorso URL o pattern", - "rulesEnable": "Abilita Regole", - "rulesEnableDescription": "Abilita o disabilita la valutazione delle regole per questa risorsa", - "rulesResource": "Configurazione Regole Risorsa", - "rulesResourceDescription": "Configura le regole per controllare l'accesso alla tua risorsa", - "ruleSubmit": "Aggiungi Regola", - "rulesNoOne": "Nessuna regola. Aggiungi una regola usando il modulo.", - "rulesOrder": "Le regole sono valutate per priorità in ordine crescente.", - "rulesSubmit": "Salva Regole", - "resourceErrorCreate": "Errore nella creazione della risorsa", - "resourceErrorCreateDescription": "Si è verificato un errore durante la creazione della risorsa", - "resourceErrorCreateMessage": "Errore nella creazione della risorsa:", - "resourceErrorCreateMessageDescription": "Si è verificato un errore imprevisto", - "sitesErrorFetch": "Errore nel recupero dei siti", - "sitesErrorFetchDescription": "Si è verificato un errore durante il recupero dei siti", - "domainsErrorFetch": "Errore nel recupero dei domini", - "domainsErrorFetchDescription": "Si è verificato un errore durante il recupero dei domini", - "none": "Nessuno", - "unknown": "Sconosciuto", - "resources": "Risorse", - "resourcesDescription": "Le risorse sono proxy per le applicazioni in esecuzione sulla tua rete privata. Crea una risorsa per qualsiasi servizio HTTP/HTTPS o TCP/UDP raw sulla tua rete privata. Ogni risorsa deve essere collegata a un sito per abilitare la connettività privata e sicura attraverso un tunnel WireGuard crittografato.", - "resourcesWireGuardConnect": "Connettività sicura con crittografia WireGuard", - "resourcesMultipleAuthenticationMethods": "Configura molteplici metodi di autenticazione", - "resourcesUsersRolesAccess": "Controllo accessi basato su utenti e ruoli", - "resourcesErrorUpdate": "Impossibile attivare/disattivare la risorsa", - "resourcesErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento della risorsa", - "access": "Accesso", - "shareLink": "Link di Condivisione {resource}", - "resourceSelect": "Seleziona risorsa", - "shareLinks": "Link di Condivisione", - "share": "Link Condivisibili", - "shareDescription2": "Crea link condivisibili per le tue risorse. I link forniscono accesso temporaneo o illimitato alla tua risorsa. Puoi configurare la durata di scadenza del link quando lo crei.", - "shareEasyCreate": "Facile da creare e condividere", - "shareConfigurableExpirationDuration": "Durata di scadenza configurabile", - "shareSecureAndRevocable": "Sicuro e revocabile", - "nameMin": "Il nome deve essere di almeno {len} caratteri.", - "nameMax": "Il nome non deve superare i {len} caratteri.", - "sitesConfirmCopy": "Conferma di aver copiato la configurazione.", - "unknownCommand": "Comando sconosciuto", - "newtErrorFetchReleases": "Impossibile recuperare le informazioni sulla versione: {err}", - "newtErrorFetchLatest": "Errore nel recupero dell'ultima versione: {err}", - "newtEndpoint": "Endpoint Newt", - "newtId": "ID Newt", - "newtSecretKey": "Chiave Segreta Newt", - "architecture": "Architettura", - "sites": "Siti", - "siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le tue risorse interne usando l'IP del peer.", - "siteWgCompatibleAllClients": "Compatibile con tutti i client WireGuard", - "siteWgManualConfigurationRequired": "Configurazione manuale richiesta", - "userErrorNotAdminOrOwner": "L'utente non è un amministratore o proprietario", - "pangolinSettings": "Impostazioni - Pangolin", - "accessRoleYour": "Il tuo ruolo:", - "accessRoleSelect2": "Seleziona un ruolo", - "accessUserSelect": "Seleziona un utente", - "otpEmailEnter": "Inserisci un'email", - "otpEmailEnterDescription": "Premi invio per aggiungere un'email dopo averla digitata nel campo di input.", - "otpEmailErrorInvalid": "Indirizzo email non valido. Il carattere jolly (*) deve essere l'intera parte locale.", - "otpEmailSmtpRequired": "SMTP Richiesto", - "otpEmailSmtpRequiredDescription": "SMTP deve essere abilitato sul server per utilizzare l'autenticazione con password monouso.", - "otpEmailTitle": "Password Monouso", - "otpEmailTitleDescription": "Richiedi autenticazione basata su email per l'accesso alle risorse", - "otpEmailWhitelist": "Lista Autorizzazioni Email", - "otpEmailWhitelistList": "Email Autorizzate", - "otpEmailWhitelistListDescription": "Solo gli utenti con questi indirizzi email potranno accedere a questa risorsa. Verrà richiesto loro di inserire una password monouso inviata alla loro email. I caratteri jolly (*@example.com) possono essere utilizzati per consentire qualsiasi indirizzo email da un dominio.", - "otpEmailWhitelistSave": "Salva Lista Autorizzazioni", - "passwordAdd": "Aggiungi Password", - "passwordRemove": "Rimuovi Password", - "pincodeAdd": "Aggiungi Codice PIN", - "pincodeRemove": "Rimuovi Codice PIN", - "resourceAuthMethods": "Metodi di Autenticazione", - "resourceAuthMethodsDescriptions": "Consenti l'accesso alla risorsa tramite metodi di autenticazione aggiuntivi", - "resourceAuthSettingsSave": "Salvato con successo", - "resourceAuthSettingsSaveDescription": "Le impostazioni di autenticazione sono state salvate", - "resourceErrorAuthFetch": "Impossibile recuperare i dati", - "resourceErrorAuthFetchDescription": "Si è verificato un errore durante il recupero dei dati", - "resourceErrorPasswordRemove": "Errore nella rimozione della password della risorsa", - "resourceErrorPasswordRemoveDescription": "Si è verificato un errore durante la rimozione della password della risorsa", - "resourceErrorPasswordSetup": "Errore nell'impostazione della password della risorsa", - "resourceErrorPasswordSetupDescription": "Si è verificato un errore durante l'impostazione della password della risorsa", - "resourceErrorPincodeRemove": "Errore nella rimozione del codice PIN della risorsa", - "resourceErrorPincodeRemoveDescription": "Si è verificato un errore durante la rimozione del codice PIN della risorsa", - "resourceErrorPincodeSetup": "Errore nell'impostazione del codice PIN della risorsa", - "resourceErrorPincodeSetupDescription": "Si è verificato un errore durante l'impostazione del codice PIN della risorsa", - "resourceErrorUsersRolesSave": "Impossibile impostare i ruoli", - "resourceErrorUsersRolesSaveDescription": "Si è verificato un errore durante l'impostazione dei ruoli", - "resourceErrorWhitelistSave": "Impossibile salvare la lista autorizzazioni", - "resourceErrorWhitelistSaveDescription": "Si è verificato un errore durante il salvataggio della lista autorizzazioni", - "resourcePasswordSubmit": "Abilita Protezione Password", - "resourcePasswordProtection": "Protezione Password {status}", - "resourcePasswordRemove": "Password della risorsa rimossa", - "resourcePasswordRemoveDescription": "La password della risorsa è stata rimossa con successo", - "resourcePasswordSetup": "Password della risorsa impostata", - "resourcePasswordSetupDescription": "La password della risorsa è stata impostata con successo", - "resourcePasswordSetupTitle": "Imposta Password", - "resourcePasswordSetupTitleDescription": "Imposta una password per proteggere questa risorsa", - "resourcePincode": "Codice PIN", - "resourcePincodeSubmit": "Abilita Protezione Codice PIN", - "resourcePincodeProtection": "Protezione Codice PIN {status}", - "resourcePincodeRemove": "Codice PIN della risorsa rimosso", - "resourcePincodeRemoveDescription": "Il codice PIN della risorsa è stato rimosso con successo", - "resourcePincodeSetup": "Codice PIN della risorsa impostato", - "resourcePincodeSetupDescription": "Il codice PIN della risorsa è stato impostato con successo", - "resourcePincodeSetupTitle": "Imposta Codice PIN", - "resourcePincodeSetupTitleDescription": "Imposta un codice PIN per proteggere questa risorsa", - "resourceRoleDescription": "Gli amministratori possono sempre accedere a questa risorsa.", - "resourceUsersRoles": "Utenti e Ruoli", - "resourceUsersRolesDescription": "Configura quali utenti e ruoli possono visitare questa risorsa", - "resourceUsersRolesSubmit": "Salva Utenti e Ruoli", - "resourceWhitelistSave": "Salvato con successo", - "resourceWhitelistSaveDescription": "Le impostazioni della lista autorizzazioni sono state salvate", - "ssoUse": "Usa SSO della Piattaforma", - "ssoUseDescription": "Gli utenti esistenti dovranno accedere solo una volta per tutte le risorse che hanno questa opzione abilitata.", - "proxyErrorInvalidPort": "Numero porta non valido", - "subdomainErrorInvalid": "Sottodominio non valido", - "domainErrorFetch": "Errore nel recupero dei domini", - "domainErrorFetchDescription": "Si è verificato un errore durante il recupero dei domini", - "resourceErrorUpdate": "Impossibile aggiornare la risorsa", - "resourceErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento della risorsa", - "resourceUpdated": "Risorsa aggiornata", - "resourceUpdatedDescription": "La risorsa è stata aggiornata con successo", - "resourceErrorTransfer": "Impossibile trasferire la risorsa", - "resourceErrorTransferDescription": "Si è verificato un errore durante il trasferimento della risorsa", - "resourceTransferred": "Risorsa trasferita", - "resourceTransferredDescription": "La risorsa è stata trasferita con successo", - "resourceErrorToggle": "Impossibile alternare la risorsa", - "resourceErrorToggleDescription": "Si è verificato un errore durante l'aggiornamento della risorsa", - "resourceVisibilityTitle": "Visibilità", - "resourceVisibilityTitleDescription": "Abilita o disabilita completamente la visibilità della risorsa", - "resourceGeneral": "Impostazioni Generali", - "resourceGeneralDescription": "Configura le impostazioni generali per questa risorsa", - "resourceEnable": "Abilita Risorsa", - "resourceTransfer": "Trasferisci Risorsa", - "resourceTransferDescription": "Trasferisci questa risorsa a un sito diverso", - "resourceTransferSubmit": "Trasferisci Risorsa", - "siteDestination": "Sito Di Destinazione", - "searchSites": "Cerca siti", - "accessRoleCreate": "Crea Ruolo", - "accessRoleCreateDescription": "Crea un nuovo ruolo per raggruppare gli utenti e gestire i loro permessi.", - "accessRoleCreateSubmit": "Crea Ruolo", - "accessRoleCreated": "Ruolo creato", - "accessRoleCreatedDescription": "Il ruolo è stato creato con successo.", - "accessRoleErrorCreate": "Impossibile creare il ruolo", - "accessRoleErrorCreateDescription": "Si è verificato un errore durante la creazione del ruolo.", - "accessRoleErrorNewRequired": "Nuovo ruolo richiesto", - "accessRoleErrorRemove": "Impossibile rimuovere il ruolo", - "accessRoleErrorRemoveDescription": "Si è verificato un errore durante la rimozione del ruolo.", - "accessRoleName": "Nome Del Ruolo", - "accessRoleQuestionRemove": "Stai per eliminare il ruolo {name}. Non puoi annullare questa azione.", - "accessRoleRemove": "Rimuovi Ruolo", - "accessRoleRemoveDescription": "Rimuovi un ruolo dall'organizzazione", - "accessRoleRemoveSubmit": "Rimuovi Ruolo", - "accessRoleRemoved": "Ruolo rimosso", - "accessRoleRemovedDescription": "Il ruolo è stato rimosso con successo.", - "accessRoleRequiredRemove": "Prima di eliminare questo ruolo, seleziona un nuovo ruolo a cui trasferire i membri esistenti.", - "manage": "Gestisci", - "sitesNotFound": "Nessun sito trovato.", - "pangolinServerAdmin": "Server Admin - Pangolina", - "licenseTierProfessional": "Licenza Professional", - "licenseTierEnterprise": "Licenza Enterprise", - "licenseTierPersonal": "Personal License", - "licensed": "Con Licenza", - "yes": "Sì", - "no": "No", - "sitesAdditional": "Siti Aggiuntivi", - "licenseKeys": "Chiavi di Licenza", - "sitestCountDecrease": "Diminuisci conteggio siti", - "sitestCountIncrease": "Aumenta conteggio siti", - "idpManage": "Gestisci Provider di Identità", - "idpManageDescription": "Visualizza e gestisci i provider di identità nel sistema", - "idpDeletedDescription": "Provider di identità eliminato con successo", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Sei sicuro di voler eliminare definitivamente il provider di identità {name}?", - "idpMessageRemove": "Questo rimuoverà il provider di identità e tutte le configurazioni associate. Gli utenti che si autenticano tramite questo provider non potranno più accedere.", - "idpMessageConfirm": "Per confermare, digita il nome del provider di identità qui sotto.", - "idpConfirmDelete": "Conferma Eliminazione Provider di Identità", - "idpDelete": "Elimina Provider di Identità", - "idp": "Provider Di Identità", - "idpSearch": "Cerca provider di identità...", - "idpAdd": "Aggiungi Provider di Identità", - "idpClientIdRequired": "L'ID client è obbligatorio.", - "idpClientSecretRequired": "Il segreto client è obbligatorio.", - "idpErrorAuthUrlInvalid": "L'URL di autenticazione deve essere un URL valido.", - "idpErrorTokenUrlInvalid": "L'URL del token deve essere un URL valido.", - "idpPathRequired": "Il percorso identificativo è obbligatorio.", - "idpScopeRequired": "Gli scope sono obbligatori.", - "idpOidcDescription": "Configura un provider di identità OpenID Connect", - "idpCreatedDescription": "Provider di identità creato con successo", - "idpCreate": "Crea Provider di Identità", - "idpCreateDescription": "Configura un nuovo provider di identità per l'autenticazione degli utenti", - "idpSeeAll": "Vedi Tutti i Provider di Identità", - "idpSettingsDescription": "Configura le informazioni di base per il tuo provider di identità", - "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": "EE", - "idpType": "Tipo di Provider", - "idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare", - "idpOidcConfigure": "Configurazione OAuth2/OIDC", - "idpOidcConfigureDescription": "Configura gli endpoint e le credenziali del provider OAuth2/OIDC", - "idpClientId": "ID Client", - "idpClientIdDescription": "L'ID client OAuth2 dal tuo provider di identità", - "idpClientSecret": "Segreto Client", - "idpClientSecretDescription": "Il segreto client OAuth2 dal tuo provider di identità", - "idpAuthUrl": "URL di Autorizzazione", - "idpAuthUrlDescription": "L'URL dell'endpoint di autorizzazione OAuth2", - "idpTokenUrl": "URL del Token", - "idpTokenUrlDescription": "L'URL dell'endpoint del token OAuth2", - "idpOidcConfigureAlert": "Informazioni Importanti", - "idpOidcConfigureAlertDescription": "Dopo aver creato il provider di identità, dovrai configurare l'URL di callback nelle impostazioni del tuo provider di identità. L'URL di callback verrà fornito dopo la creazione riuscita.", - "idpToken": "Configurazione Token", - "idpTokenDescription": "Configura come estrarre le informazioni dell'utente dal token ID", - "idpJmespathAbout": "Informazioni su JMESPath", - "idpJmespathAboutDescription": "I percorsi sottostanti utilizzano la sintassi JMESPath per estrarre valori dal token ID.", - "idpJmespathAboutDescriptionLink": "Scopri di più su JMESPath", - "idpJmespathLabel": "Percorso Identificativo", - "idpJmespathLabelDescription": "Il JMESPath per l'identificatore dell'utente nel token ID", - "idpJmespathEmailPathOptional": "Percorso Email (Opzionale)", - "idpJmespathEmailPathOptionalDescription": "Il JMESPath per l'email dell'utente nel token ID", - "idpJmespathNamePathOptional": "Percorso Nome (Opzionale)", - "idpJmespathNamePathOptionalDescription": "Il JMESPath per il nome dell'utente nel token ID", - "idpOidcConfigureScopes": "Scope", - "idpOidcConfigureScopesDescription": "Lista degli scope OAuth2 da richiedere separati da spazi", - "idpSubmit": "Crea Provider di Identità", - "orgPolicies": "Politiche Organizzazione", - "idpSettings": "Impostazioni {idpName}", - "idpCreateSettingsDescription": "Configura le impostazioni per il tuo provider di identità", - "roleMapping": "Mappatura Ruoli", - "orgMapping": "Mappatura Organizzazione", - "orgPoliciesSearch": "Cerca politiche organizzazione...", - "orgPoliciesAdd": "Aggiungi Politica Organizzazione", - "orgRequired": "L'organizzazione è obbligatoria", - "error": "Errore", - "success": "Successo", - "orgPolicyAddedDescription": "Politica aggiunta con successo", - "orgPolicyUpdatedDescription": "Politica aggiornata con successo", - "orgPolicyDeletedDescription": "Politica eliminata con successo", - "defaultMappingsUpdatedDescription": "Mappature predefinite aggiornate con successo", - "orgPoliciesAbout": "Informazioni sulle Politiche Organizzazione", - "orgPoliciesAboutDescription": "Le politiche organizzazione sono utilizzate per controllare l'accesso alle organizzazioni in base al token ID dell'utente. Puoi specificare espressioni JMESPath per estrarre informazioni su ruoli e organizzazioni dal token ID. Per maggiori informazioni, vedi", - "orgPoliciesAboutDescriptionLink": "la documentazione", - "defaultMappingsOptional": "Mappature Predefinite (Opzionale)", - "defaultMappingsOptionalDescription": "Le mappature predefinite sono utilizzate quando non esiste una politica organizzazione definita per un'organizzazione. Puoi specificare qui le mappature predefinite di ruolo e organizzazione da utilizzare come fallback.", - "defaultMappingsRole": "Mappatura Ruolo Predefinito", - "defaultMappingsRoleDescription": "JMESPath per estrarre informazioni sul ruolo dal token ID. Il risultato di questa espressione deve restituire il nome del ruolo come definito nell'organizzazione come stringa.", - "defaultMappingsOrg": "Mappatura Organizzazione Predefinita", - "defaultMappingsOrgDescription": "JMESPath per estrarre informazioni sull'organizzazione dal token ID. Questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere all'organizzazione.", - "defaultMappingsSubmit": "Salva Mappature Predefinite", - "orgPoliciesEdit": "Modifica Politica Organizzazione", - "org": "Organizzazione", - "orgSelect": "Seleziona organizzazione", - "orgSearch": "Cerca organizzazione", - "orgNotFound": "Nessuna organizzazione trovata.", - "roleMappingPathOptional": "Percorso Mappatura Ruolo (Opzionale)", - "orgMappingPathOptional": "Percorso Mappatura Organizzazione (Opzionale)", - "orgPolicyUpdate": "Aggiorna Politica", - "orgPolicyAdd": "Aggiungi Politica", - "orgPolicyConfig": "Configura l'accesso per un'organizzazione", - "idpUpdatedDescription": "Provider di identità aggiornato con successo", - "redirectUrl": "URL di Reindirizzamento", - "redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento", - "redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti verranno reindirizzati dopo l'autenticazione. Devi configurare questo URL nelle impostazioni del tuo provider di identità.", - "pangolinAuth": "Autenticazione - Pangolina", - "verificationCodeLengthRequirements": "Il tuo codice di verifica deve essere di 8 caratteri.", - "errorOccurred": "Si è verificato un errore", - "emailErrorVerify": "Impossibile verificare l'email:", - "emailVerified": "Email verificata con successo! Reindirizzamento in corso...", - "verificationCodeErrorResend": "Impossibile reinviare il codice di verifica:", - "verificationCodeResend": "Codice di verifica reinviato", - "verificationCodeResendDescription": "Abbiamo reinviato un codice di verifica al tuo indirizzo email. Controlla la tua casella di posta.", - "emailVerify": "Verifica Email", - "emailVerifyDescription": "Inserisci il codice di verifica inviato al tuo indirizzo email.", - "verificationCode": "Codice di Verifica", - "verificationCodeEmailSent": "Abbiamo inviato un codice di verifica al tuo indirizzo email.", - "submit": "Invia", - "emailVerifyResendProgress": "Reinvio in corso...", - "emailVerifyResend": "Non hai ricevuto il codice? Clicca qui per reinviare", - "passwordNotMatch": "Le password non coincidono", - "signupError": "Si è verificato un errore durante la registrazione", - "pangolinLogoAlt": "Logo Pangolin", - "inviteAlready": "Sembra che sei stato invitato!", - "inviteAlreadyDescription": "Per accettare l'invito, devi accedere o creare un account.", - "signupQuestion": "Hai già un account?", - "login": "Accedi", - "resourceNotFound": "Risorsa Non Trovata", - "resourceNotFoundDescription": "La risorsa che stai cercando di accedere non esiste.", - "pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre", - "pincodeRequirementsChars": "Il PIN deve contenere solo numeri", - "passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere", - "passwordRequirementsTitle": "Requisiti della password:", - "passwordRequirementLength": "Almeno 8 caratteri", - "passwordRequirementUppercase": "Almeno una lettera maiuscola", - "passwordRequirementLowercase": "Almeno una lettera minuscola", - "passwordRequirementNumber": "Almeno un numero", - "passwordRequirementSpecial": "Almeno un carattere speciale", - "passwordRequirementsMet": "✓ La password soddisfa tutti i requisiti", - "passwordStrength": "Forza della password", - "passwordStrengthWeak": "Debole", - "passwordStrengthMedium": "Media", - "passwordStrengthStrong": "Forte", - "passwordRequirements": "Requisiti:", - "passwordRequirementLengthText": "8+ caratteri", - "passwordRequirementUppercaseText": "Lettera maiuscola (A-Z)", - "passwordRequirementLowercaseText": "Lettera minuscola (a-z)", - "passwordRequirementNumberText": "Numero (0-9)", - "passwordRequirementSpecialText": "Carattere speciale (!@#$%...)", - "passwordsDoNotMatch": "Le password non coincidono", - "otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere", - "otpEmailSent": "OTP Inviato", - "otpEmailSentDescription": "Un OTP è stato inviato alla tua email", - "otpEmailErrorAuthenticate": "Impossibile autenticare con l'email", - "pincodeErrorAuthenticate": "Impossibile autenticare con il codice PIN", - "passwordErrorAuthenticate": "Impossibile autenticare con la password", - "poweredBy": "Offerto da", - "authenticationRequired": "Autenticazione Richiesta", - "authenticationMethodChoose": "Scegli il tuo metodo preferito per accedere a {name}", - "authenticationRequest": "Devi autenticarti per accedere a {name}", - "user": "Utente", - "pincodeInput": "Codice PIN a 6 cifre", - "pincodeSubmit": "Accedi con PIN", - "passwordSubmit": "Accedi con Password", - "otpEmailDescription": "Un codice usa e getta verrà inviato a questa email.", - "otpEmailSend": "Invia Codice Usa e Getta", - "otpEmail": "Password Usa e Getta (OTP)", - "otpEmailSubmit": "Invia OTP", - "backToEmail": "Torna all'Email", - "noSupportKey": "Il server è in esecuzione senza una chiave di supporto. Considera di supportare il progetto!", - "accessDenied": "Accesso Negato", - "accessDeniedDescription": "Non sei autorizzato ad accedere a questa risorsa. Se ritieni che sia un errore, contatta l'amministratore.", - "accessTokenError": "Errore nel controllo del token di accesso", - "accessGranted": "Accesso Concesso", - "accessUrlInvalid": "URL di Accesso Non Valido", - "accessGrantedDescription": "Ti è stato concesso l'accesso a questa risorsa. Reindirizzamento in corso...", - "accessUrlInvalidDescription": "Questo URL di accesso condiviso non è valido. Contatta il proprietario della risorsa per un nuovo URL.", - "tokenInvalid": "Token non valido", - "pincodeInvalid": "Codice non valido", - "passwordErrorRequestReset": "Impossibile richiedere il reset:", - "passwordErrorReset": "Impossibile reimpostare la password:", - "passwordResetSuccess": "Password reimpostata con successo! Torna al login...", - "passwordReset": "Reimposta Password", - "passwordResetDescription": "Segui i passaggi per reimpostare la tua password", - "passwordResetSent": "Invieremo un codice di reset della password a questo indirizzo email.", - "passwordResetCode": "Codice di Reset", - "passwordResetCodeDescription": "Controlla la tua email per il codice di reset.", - "passwordNew": "Nuova Password", - "passwordNewConfirm": "Conferma Nuova Password", - "pincodeAuth": "Codice Autenticatore", - "pincodeSubmit2": "Invia Codice", - "passwordResetSubmit": "Richiedi Reset", - "passwordBack": "Torna alla Password", - "loginBack": "Torna al login", - "signup": "Registrati", - "loginStart": "Accedi per iniziare", - "idpOidcTokenValidating": "Convalida token OIDC", - "idpOidcTokenResponse": "Convalida risposta token OIDC", - "idpErrorOidcTokenValidating": "Errore nella convalida del token OIDC", - "idpConnectingTo": "Connessione a {name}", - "idpConnectingToDescription": "Convalida della tua identità", - "idpConnectingToProcess": "Connessione in corso...", - "idpConnectingToFinished": "Connesso", - "idpErrorConnectingTo": "Si è verificato un problema durante la connessione a {name}. Contatta il tuo amministratore.", - "idpErrorNotFound": "IdP non trovato", - "inviteInvalid": "Invito Non Valido", - "inviteInvalidDescription": "Il link di invito non è valido.", - "inviteErrorWrongUser": "L'invito non è per questo utente", - "inviteErrorUserNotExists": "L'utente non esiste. Si prega di creare prima un account.", - "inviteErrorLoginRequired": "Devi effettuare l'accesso per accettare un invito", - "inviteErrorExpired": "L'invito potrebbe essere scaduto", - "inviteErrorRevoked": "L'invito potrebbe essere stato revocato", - "inviteErrorTypo": "Potrebbe esserci un errore di battitura nel link di invito", - "pangolinSetup": "Configurazione - Pangolin", - "orgNameRequired": "Il nome dell'organizzazione è obbligatorio", - "orgIdRequired": "L'ID dell'organizzazione è obbligatorio", - "orgErrorCreate": "Si è verificato un errore durante la creazione dell'organizzazione", - "pageNotFound": "Pagina Non Trovata", - "pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.", - "overview": "Panoramica", - "home": "Home", - "accessControl": "Controllo Accessi", - "settings": "Impostazioni", - "usersAll": "Tutti Gli Utenti", - "license": "Licenza", - "pangolinDashboard": "Cruscotto - Pangolino", - "noResults": "Nessun risultato trovato.", - "terabytes": "{count} TB", - "gigabytes": "{count}GB", - "megabytes": "{count} MB", - "tagsEntered": "Tag Inseriti", - "tagsEnteredDescription": "Questi sono i tag che hai inserito.", - "tagsWarnCannotBeLessThanZero": "maxTags e minTags non possono essere minori di 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag non consentito come da opzioni di autocompletamento", - "tagsWarnInvalid": "Tag non valido secondo validateTag", - "tagWarnTooShort": "Il tag {tagText} è troppo corto", - "tagWarnTooLong": "Il tag {tagText} è troppo lungo", - "tagsWarnReachedMaxNumber": "Raggiunto il numero massimo di tag consentiti", - "tagWarnDuplicate": "Tag duplicato {tagText} non aggiunto", - "supportKeyInvalid": "Chiave Non Valida", - "supportKeyInvalidDescription": "La tua chiave di supporto non è valida.", - "supportKeyValid": "Chiave Valida", - "supportKeyValidDescription": "La tua chiave di supporto è stata convalidata. Grazie per il tuo sostegno!", - "supportKeyErrorValidationDescription": "Impossibile convalidare la chiave di supporto.", - "supportKey": "Supporta lo Sviluppo e Adotta un Pangolino!", - "supportKeyDescription": "Acquista una chiave di supporto per aiutarci a continuare a sviluppare Pangolin per la comunità. Il tuo contributo ci permette di dedicare più tempo alla manutenzione e all'aggiunta di nuove funzionalità per tutti. Non useremo mai questo per bloccare le funzionalità. Questo è separato da qualsiasi Edizione Commerciale.", - "supportKeyPet": "Potrai anche adottare e incontrare il tuo pangolino personale!", - "supportKeyPurchase": "I pagamenti sono elaborati tramite GitHub. Successivamente, potrai recuperare la tua chiave su", - "supportKeyPurchaseLink": "il nostro sito web", - "supportKeyPurchase2": "e riscattarla qui.", - "supportKeyLearnMore": "Scopri di più.", - "supportKeyOptions": "Seleziona l'opzione più adatta a te.", - "supportKetOptionFull": "Supporto Completo", - "forWholeServer": "Per l'intero server", - "lifetimePurchase": "Acquisto a vita", - "supporterStatus": "Stato supportatore", - "buy": "Acquista", - "supportKeyOptionLimited": "Supporto Limitato", - "forFiveUsers": "Per 5 o meno utenti", - "supportKeyRedeem": "Riscatta Chiave di Supporto", - "supportKeyHideSevenDays": "Nascondi per 7 giorni", - "supportKeyEnter": "Inserisci Chiave di Supporto", - "supportKeyEnterDescription": "Incontra il tuo pangolino personale!", - "githubUsername": "Username GitHub", - "supportKeyInput": "Chiave di Supporto", - "supportKeyBuy": "Acquista Chiave di Supporto", - "logoutError": "Errore durante il logout", - "signingAs": "Accesso come", - "serverAdmin": "Amministratore Server", - "managedSelfhosted": "Gestito Auto-Ospitato", - "otpEnable": "Abilita Autenticazione a Due Fattori", - "otpDisable": "Disabilita Autenticazione a Due Fattori", - "logout": "Disconnetti", - "licenseTierProfessionalRequired": "Edizione Professional Richiesta", - "licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.", - "actionGetOrg": "Ottieni Organizzazione", - "updateOrgUser": "Aggiorna Utente Org", - "createOrgUser": "Crea Utente Org", - "actionUpdateOrg": "Aggiorna Organizzazione", - "actionUpdateUser": "Aggiorna Utente", - "actionGetUser": "Ottieni Utente", - "actionGetOrgUser": "Ottieni Utente Organizzazione", - "actionListOrgDomains": "Elenca Domini Organizzazione", - "actionCreateSite": "Crea Sito", - "actionDeleteSite": "Elimina Sito", - "actionGetSite": "Ottieni Sito", - "actionListSites": "Elenca Siti", - "actionApplyBlueprint": "Applica Progetto", - "setupToken": "Configura Token", - "setupTokenDescription": "Inserisci il token di configurazione dalla console del server.", - "setupTokenRequired": "Il token di configurazione è richiesto", - "actionUpdateSite": "Aggiorna Sito", - "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", - "actionCreateResource": "Crea Risorsa", - "actionDeleteResource": "Elimina Risorsa", - "actionGetResource": "Ottieni Risorsa", - "actionListResource": "Elenca Risorse", - "actionUpdateResource": "Aggiorna Risorsa", - "actionListResourceUsers": "Elenca Utenti Risorsa", - "actionSetResourceUsers": "Imposta Utenti Risorsa", - "actionSetAllowedResourceRoles": "Imposta Ruoli Risorsa Consentiti", - "actionListAllowedResourceRoles": "Elenca Ruoli Risorsa Consentiti", - "actionSetResourcePassword": "Imposta Password Risorsa", - "actionSetResourcePincode": "Imposta Codice PIN Risorsa", - "actionSetResourceEmailWhitelist": "Imposta Lista Autorizzazioni Email Risorsa", - "actionGetResourceEmailWhitelist": "Ottieni Lista Autorizzazioni Email Risorsa", - "actionCreateTarget": "Crea Target", - "actionDeleteTarget": "Elimina Target", - "actionGetTarget": "Ottieni Target", - "actionListTargets": "Elenca Target", - "actionUpdateTarget": "Aggiorna Target", - "actionCreateRole": "Crea Ruolo", - "actionDeleteRole": "Elimina Ruolo", - "actionGetRole": "Ottieni Ruolo", - "actionListRole": "Elenca Ruoli", - "actionUpdateRole": "Aggiorna Ruolo", - "actionListAllowedRoleResources": "Elenca Risorse Ruolo Consentite", - "actionInviteUser": "Invita Utente", - "actionRemoveUser": "Rimuovi Utente", - "actionListUsers": "Elenca Utenti", - "actionAddUserRole": "Aggiungi Ruolo Utente", - "actionGenerateAccessToken": "Genera Token di Accesso", - "actionDeleteAccessToken": "Elimina Token di Accesso", - "actionListAccessTokens": "Elenca Token di Accesso", - "actionCreateResourceRule": "Crea Regola Risorsa", - "actionDeleteResourceRule": "Elimina Regola Risorsa", - "actionListResourceRules": "Elenca Regole Risorsa", - "actionUpdateResourceRule": "Aggiorna Regola Risorsa", - "actionListOrgs": "Elenca Organizzazioni", - "actionCheckOrgId": "Controlla ID", - "actionCreateOrg": "Crea Organizzazione", - "actionDeleteOrg": "Elimina Organizzazione", - "actionListApiKeys": "Elenca Chiavi API", - "actionListApiKeyActions": "Elenca Azioni Chiave API", - "actionSetApiKeyActions": "Imposta Azioni Consentite Chiave API", - "actionCreateApiKey": "Crea Chiave API", - "actionDeleteApiKey": "Elimina Chiave API", - "actionCreateIdp": "Crea IDP", - "actionUpdateIdp": "Aggiorna IDP", - "actionDeleteIdp": "Elimina IDP", - "actionListIdps": "Elenca IDP", - "actionGetIdp": "Ottieni IDP", - "actionCreateIdpOrg": "Crea Politica Org IDP", - "actionDeleteIdpOrg": "Elimina Politica Org IDP", - "actionListIdpOrgs": "Elenca Org IDP", - "actionUpdateIdpOrg": "Aggiorna Org IDP", - "actionCreateClient": "Crea Client", - "actionDeleteClient": "Elimina Client", - "actionUpdateClient": "Aggiorna Client", - "actionListClients": "Elenco Clienti", - "actionGetClient": "Ottieni Client", - "actionCreateSiteResource": "Crea Risorsa del Sito", - "actionDeleteSiteResource": "Elimina Risorsa del Sito", - "actionGetSiteResource": "Ottieni Risorsa del Sito", - "actionListSiteResources": "Elenca Risorse del Sito", - "actionUpdateSiteResource": "Aggiorna Risorsa del Sito", - "actionListInvitations": "Elenco Inviti", - "noneSelected": "Nessuna selezione", - "orgNotFound2": "Nessuna organizzazione trovata.", - "searchProgress": "Ricerca...", - "create": "Crea", - "orgs": "Organizzazioni", - "loginError": "Si è verificato un errore durante l'accesso", - "passwordForgot": "Password dimenticata?", - "otpAuth": "Autenticazione a Due Fattori", - "otpAuthDescription": "Inserisci il codice dalla tua app di autenticazione o uno dei tuoi codici di backup monouso.", - "otpAuthSubmit": "Invia Codice", - "idpContinue": "O continua con", - "otpAuthBack": "Torna al Login", - "navbar": "Menu di Navigazione", - "navbarDescription": "Menu di navigazione principale dell'applicazione", - "navbarDocsLink": "Documentazione", - "otpErrorEnable": "Impossibile abilitare 2FA", - "otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA", - "otpSetupCheckCode": "Inserisci un codice a 6 cifre", - "otpSetupCheckCodeRetry": "Codice non valido. Riprova.", - "otpSetup": "Abilita Autenticazione a Due Fattori", - "otpSetupDescription": "Proteggi il tuo account con un livello extra di protezione", - "otpSetupScanQr": "Scansiona questo codice QR con la tua app di autenticazione o inserisci manualmente la chiave segreta:", - "otpSetupSecretCode": "Codice Autenticatore", - "otpSetupSuccess": "Autenticazione a Due Fattori Abilitata", - "otpSetupSuccessStoreBackupCodes": "Il tuo account è ora più sicuro. Non dimenticare di salvare i tuoi codici di backup.", - "otpErrorDisable": "Impossibile disabilitare 2FA", - "otpErrorDisableDescription": "Si è verificato un errore durante la disabilitazione di 2FA", - "otpRemove": "Disabilita Autenticazione a Due Fattori", - "otpRemoveDescription": "Disabilita l'autenticazione a due fattori per il tuo account", - "otpRemoveSuccess": "Autenticazione a Due Fattori Disabilitata", - "otpRemoveSuccessMessage": "L'autenticazione a due fattori è stata disabilitata per il tuo account. Puoi riattivarla in qualsiasi momento.", - "otpRemoveSubmit": "Disabilita 2FA", - "paginator": "Pagina {current} di {last}", - "paginatorToFirst": "Vai alla prima pagina", - "paginatorToPrevious": "Vai alla pagina precedente", - "paginatorToNext": "Vai alla pagina successiva", - "paginatorToLast": "Vai all'ultima pagina", - "copyText": "Copia testo", - "copyTextFailed": "Impossibile copiare il testo: ", - "copyTextClipboard": "Copia negli appunti", - "inviteErrorInvalidConfirmation": "Conferma non valida", - "passwordRequired": "La password è obbligatoria", - "allowAll": "Consenti Tutto", - "permissionsAllowAll": "Consenti Tutti I Permessi", - "githubUsernameRequired": "È richiesto l'username GitHub", - "supportKeyRequired": "È richiesta la chiave di supporto", - "passwordRequirementsChars": "La password deve essere di almeno 8 caratteri", - "language": "Lingua", - "verificationCodeRequired": "È richiesto un codice", - "userErrorNoUpdate": "Nessun utente da aggiornare", - "siteErrorNoUpdate": "Nessun sito da aggiornare", - "resourceErrorNoUpdate": "Nessuna risorsa da aggiornare", - "authErrorNoUpdate": "Nessuna informazione di autenticazione da aggiornare", - "orgErrorNoUpdate": "Nessuna organizzazione da aggiornare", - "orgErrorNoProvided": "Nessuna organizzazione fornita", - "apiKeysErrorNoUpdate": "Nessuna chiave API da aggiornare", - "sidebarOverview": "Panoramica", - "sidebarHome": "Home", - "sidebarSites": "Siti", - "sidebarResources": "Risorse", - "sidebarAccessControl": "Controllo Accesso", - "sidebarUsers": "Utenti", - "sidebarInvitations": "Inviti", - "sidebarRoles": "Ruoli", - "sidebarShareableLinks": "Collegamenti Condividibili", - "sidebarApiKeys": "Chiavi API", - "sidebarSettings": "Impostazioni", - "sidebarAllUsers": "Tutti Gli Utenti", - "sidebarIdentityProviders": "Fornitori Di Identità", - "sidebarLicense": "Licenza", - "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.", - "enableDockerSocketLink": "Scopri di più", - "viewDockerContainers": "Visualizza Contenitori Docker", - "containersIn": "Contenitori in {siteName}", - "selectContainerDescription": "Seleziona qualsiasi contenitore da usare come hostname per questo obiettivo. Fai clic su una porta per usare una porta.", - "containerName": "Nome", - "containerImage": "Immagine", - "containerState": "Stato", - "containerNetworks": "Reti", - "containerHostnameIp": "Hostname/IP", - "containerLabels": "Etichette", - "containerLabelsCount": "{count, plural, one {# etichetta} other {# etichette}}", - "containerLabelsTitle": "Etichette Del Contenitore", - "containerLabelEmpty": "", - "containerPorts": "Porte", - "containerPortsMore": "+{count} in più", - "containerActions": "Azioni", - "select": "Seleziona", - "noContainersMatchingFilters": "Nessun contenitore trovato corrispondente ai filtri correnti.", - "showContainersWithoutPorts": "Mostra contenitori senza porte", - "showStoppedContainers": "Mostra contenitori fermati", - "noContainersFound": "Nessun contenitore trovato. Assicurarsi che i contenitori Docker siano in esecuzione.", - "searchContainersPlaceholder": "Cerca tra i contenitori {count}...", - "searchResultsCount": "{count, plural, one {# risultato} other {# risultati}}", - "filters": "Filtri", - "filterOptions": "Opzioni Filtro", - "filterPorts": "Porte", - "filterStopped": "Fermato", - "clearAllFilters": "Cancella tutti i filtri", - "columns": "Colonne", - "toggleColumns": "Attiva/Disattiva Colonne", - "refreshContainersList": "Aggiorna elenco contenitori", - "searching": "Ricerca...", - "noContainersFoundMatching": "Nessun contenitore trovato corrispondente \"{filter}\".", - "light": "chiaro", - "dark": "scuro", - "system": "sistema", - "theme": "Tema", - "subnetRequired": "Sottorete richiesta", - "initialSetupTitle": "Impostazione Iniziale del Server", - "initialSetupDescription": "Crea l'account amministratore del server iniziale. Può esistere solo un amministratore del server. È sempre possibile modificare queste credenziali in seguito.", - "createAdminAccount": "Crea Account Admin", - "setupErrorCreateAdmin": "Si è verificato un errore durante la creazione dell'account amministratore del server.", - "certificateStatus": "Stato del Certificato", - "loading": "Caricamento", - "restart": "Riavvia", - "domains": "Domini", - "domainsDescription": "Gestisci domini per la tua organizzazione", - "domainsSearch": "Cerca domini...", - "domainAdd": "Aggiungi Dominio", - "domainAddDescription": "Registra un nuovo dominio con la tua organizzazione", - "domainCreate": "Crea Dominio", - "domainCreatedDescription": "Dominio creato con successo", - "domainDeletedDescription": "Dominio eliminato con successo", - "domainQuestionRemove": "Sei sicuro di voler rimuovere il dominio {domain} dal tuo account?", - "domainMessageRemove": "Una volta rimosso, il dominio non sarà più associato al tuo account.", - "domainMessageConfirm": "Per confermare, digita il nome del dominio qui sotto.", - "domainConfirmDelete": "Conferma Eliminazione Dominio", - "domainDelete": "Elimina Dominio", - "domain": "Dominio", - "selectDomainTypeNsName": "Delega Dominio (NS)", - "selectDomainTypeNsDescription": "Questo dominio e tutti i suoi sottodomini. Usa questo quando desideri controllare un'intera zona di dominio.", - "selectDomainTypeCnameName": "Dominio Singolo (CNAME)", - "selectDomainTypeCnameDescription": "Solo questo dominio specifico. Usa questo per sottodomini individuali o specifiche voci di dominio.", - "selectDomainTypeWildcardName": "Dominio Jolly", - "selectDomainTypeWildcardDescription": "Questo dominio e i suoi sottodomini.", - "domainDelegation": "Dominio Singolo", - "selectType": "Seleziona un tipo", - "actions": "Azioni", - "refresh": "Aggiorna", - "refreshError": "Impossibile aggiornare i dati", - "verified": "Verificato", - "pending": "In attesa", - "sidebarBilling": "Fatturazione", - "billing": "Fatturazione", - "orgBillingDescription": "Gestisci le tue informazioni di fatturazione e abbonamenti", - "github": "GitHub", - "pangolinHosted": "Pangolin Hosted", - "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.", - "accountSetupCode": "Codice di Configurazione", - "accountSetupCodeDescription": "Controlla la tua email per il codice di configurazione.", - "passwordCreate": "Crea Password", - "passwordCreateConfirm": "Conferma Password", - "accountSetupSubmit": "Invia Codice di Configurazione", - "completeSetup": "Completa la Configurazione", - "accountSetupSuccess": "Configurazione dell'account completata! Benvenuto su Pangolin!", - "documentation": "Documentazione", - "saveAllSettings": "Salva Tutte le Impostazioni", - "settingsUpdated": "Impostazioni aggiornate", - "settingsUpdatedDescription": "Tutte le impostazioni sono state aggiornate con successo", - "settingsErrorUpdate": "Impossibile aggiornare le impostazioni", - "settingsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni", - "sidebarCollapse": "Comprimi", - "sidebarExpand": "Espandi", - "newtUpdateAvailable": "Aggiornamento Disponibile", - "newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.", - "domainPickerEnterDomain": "Dominio", - "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.", - "domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili", - "domainPickerTabAll": "Tutti", - "domainPickerTabOrganization": "Organizzazione", - "domainPickerTabProvided": "Fornito", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Controllando la disponibilità...", - "domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato. Prova un dominio diverso o verifica le impostazioni del dominio della tua organizzazione.", - "domainPickerOrganizationDomains": "Domini dell'Organizzazione", - "domainPickerProvidedDomains": "Domini Forniti", - "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", - "createNewOrgDescription": "Crea una nuova organizzazione", - "organization": "Organizzazione", - "port": "Porta", - "securityKeyManage": "Gestisci chiavi di sicurezza", - "securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password", - "securityKeyRegister": "Registra nuova chiave di sicurezza", - "securityKeyList": "Le tue chiavi di sicurezza", - "securityKeyNone": "Nessuna chiave di sicurezza registrata", - "securityKeyNameRequired": "Il nome è obbligatorio", - "securityKeyRemove": "Rimuovi", - "securityKeyLastUsed": "Ultimo utilizzo: {date}", - "securityKeyNameLabel": "Nome", - "securityKeyRegisterSuccess": "Chiave di sicurezza registrata con successo", - "securityKeyRegisterError": "Errore durante la registrazione della chiave di sicurezza", - "securityKeyRemoveSuccess": "Chiave di sicurezza rimossa con successo", - "securityKeyRemoveError": "Errore durante la rimozione della chiave di sicurezza", - "securityKeyLoadError": "Errore durante il caricamento delle chiavi di sicurezza", - "securityKeyLogin": "Continua con la chiave di sicurezza", - "securityKeyAuthError": "Errore durante l'autenticazione con chiave di sicurezza", - "securityKeyRecommendation": "Considera di registrare un'altra chiave di sicurezza su un dispositivo diverso per assicurarti di non rimanere bloccato fuori dal tuo account.", - "registering": "Registrazione in corso...", - "securityKeyPrompt": "Verifica la tua identità usando la chiave di sicurezza. Assicurati che sia connessa e pronta.", - "securityKeyBrowserNotSupported": "Il tuo browser non supporta le chiavi di sicurezza. Per favore, usa un browser moderno come Chrome, Firefox o Safari.", - "securityKeyPermissionDenied": "Consenti accesso alla tua chiave di sicurezza per continuare ad accedere.", - "securityKeyRemovedTooQuickly": "Mantieni la chiave di sicurezza connessa fino a quando il processo di accesso non è completato.", - "securityKeyNotSupported": "La tua chiave di sicurezza potrebbe non essere compatibile. Prova un'altra chiave di sicurezza.", - "securityKeyUnknownError": "Si è verificato un problema con la tua chiave di sicurezza. Riprova.", - "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.", - "securityKeyAdd": "Aggiungi Chiave di Sicurezza", - "securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza", - "securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla", - "securityKeyTwoFactorRequired": "Autenticazione a Due Fattori Richiesta", - "securityKeyTwoFactorDescription": "Inserisci il codice di autenticazione a due fattori per registrare la chiave di sicurezza", - "securityKeyTwoFactorRemoveDescription": "Inserisci il codice di autenticazione a due fattori per rimuovere la chiave di sicurezza", - "securityKeyTwoFactorCode": "Codice a Due Fattori", - "securityKeyRemoveTitle": "Rimuovi Chiave di Sicurezza", - "securityKeyRemoveDescription": "Inserisci la tua password per rimuovere la chiave di sicurezza \"{name}\"", - "securityKeyNoKeysRegistered": "Nessuna chiave di sicurezza registrata", - "securityKeyNoKeysDescription": "Aggiungi una chiave di sicurezza per migliorare la sicurezza del tuo account", - "createDomainRequired": "Dominio richiesto", - "createDomainAddDnsRecords": "Aggiungi Record DNS", - "createDomainAddDnsRecordsDescription": "Aggiungi i seguenti record DNS al tuo provider di domini per completare la configurazione.", - "createDomainNsRecords": "Record NS", - "createDomainRecord": "Record", - "createDomainType": "Tipo:", - "createDomainName": "Nome:", - "createDomainValue": "Valore:", - "createDomainCnameRecords": "Record CNAME", - "createDomainARecords": "Record A", - "createDomainRecordNumber": "Record {number}", - "createDomainTxtRecords": "Record TXT", - "createDomainSaveTheseRecords": "Salva Questi Record", - "createDomainSaveTheseRecordsDescription": "Assicurati di salvare questi record DNS poiché non li vedrai più.", - "createDomainDnsPropagation": "Propagazione DNS", - "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", - "and": "e", - "privacyPolicy": "informativa sulla privacy" - }, - "siteRequired": "Il sito è richiesto.", - "olmTunnel": "Tunnel Olm", - "olmTunnelDescription": "Usa Olm per la connettività client", - "errorCreatingClient": "Errore nella creazione del client", - "clientDefaultsNotFound": "Impostazioni predefinite del client non trovate", - "createClient": "Crea Cliente", - "createClientDescription": "Crea un nuovo cliente per connettersi ai tuoi siti", - "seeAllClients": "Vedi Tutti i Clienti", - "clientInformation": "Informazioni sul Cliente", - "clientNamePlaceholder": "Nome Cliente", - "address": "Indirizzo", - "subnetPlaceholder": "Sottorete", - "addressDescription": "L'indirizzo che questo cliente utilizzerà per la connettività", - "selectSites": "Seleziona siti", - "sitesDescription": "Il cliente avrà connettività ai siti selezionati", - "clientInstallOlm": "Installa Olm", - "clientInstallOlmDescription": "Avvia Olm sul tuo sistema", - "clientOlmCredentials": "Credenziali Olm", - "clientOlmCredentialsDescription": "Ecco come Olm si autenticherà con il server", - "olmEndpoint": "Endpoint Olm", - "olmId": "ID Olm", - "olmSecretKey": "Chiave Segreta Olm", - "clientCredentialsSave": "Salva le Tue Credenziali", - "clientCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", - "generalSettingsDescription": "Configura le impostazioni generali per questo cliente", - "clientUpdated": "Cliente aggiornato", - "clientUpdatedDescription": "Il cliente è stato aggiornato.", - "clientUpdateFailed": "Impossibile aggiornare il cliente", - "clientUpdateError": "Si è verificato un errore durante l'aggiornamento del cliente.", - "sitesFetchFailed": "Impossibile recuperare i siti", - "sitesFetchError": "Si è verificato un errore durante il recupero dei siti.", - "olmErrorFetchReleases": "Si è verificato un errore durante il recupero delle versioni di Olm.", - "olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.", - "remoteSubnets": "Sottoreti Remote", - "enterCidrRange": "Inserisci l'intervallo CIDR", - "remoteSubnetsDescription": "Aggiungi intervalli CIDR che possono essere accessibili da questo sito in remoto utilizzando i client. Usa il formato come 10.0.0.0/24. Questo si applica SOLO alla connettività del client VPN.", - "resourceEnableProxy": "Abilita Proxy Pubblico", - "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", - "externalProxyEnabled": "Proxy Esterno Abilitato", - "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", - "domainPickerBaseDomainLabel": "Dominio Base", - "domainPickerSearchDomains": "Cerca domini...", - "domainPickerNoDomainsFound": "Nessun dominio trovato", - "domainPickerLoadingDomains": "Caricamento domini...", - "domainPickerSelectBaseDomain": "Seleziona dominio base...", - "domainPickerNotAvailableForCname": "Non disponibile per i domini CNAME", - "domainPickerEnterSubdomainOrLeaveBlank": "Inserisci un sottodominio o lascia vuoto per utilizzare il dominio base.", - "domainPickerEnterSubdomainToSearch": "Inserisci un sottodominio per cercare e selezionare dai domini gratuiti disponibili.", - "domainPickerFreeDomains": "Domini Gratuiti", - "domainPickerSearchForAvailableDomains": "Cerca domini disponibili", - "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", - "proxyPort": "Porta", - "resourcesTableProxyResources": "Risorse Proxy", - "resourcesTableClientResources": "Risorse Client", - "resourcesTableNoProxyResourcesFound": "Nessuna risorsa proxy trovata.", - "resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.", - "resourcesTableDestination": "Destinazione", - "resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con", - "resourcesTableClients": "Client", - "resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.", - "editInternalResourceDialogEditClientResource": "Modifica Risorsa Client", - "editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.", - "editInternalResourceDialogResourceProperties": "Proprietà della Risorsa", - "editInternalResourceDialogName": "Nome", - "editInternalResourceDialogProtocol": "Protocollo", - "editInternalResourceDialogSitePort": "Porta del Sito", - "editInternalResourceDialogTargetConfiguration": "Configurazione Target", - "editInternalResourceDialogCancel": "Annulla", - "editInternalResourceDialogSaveResource": "Salva Risorsa", - "editInternalResourceDialogSuccess": "Successo", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Risorsa interna aggiornata con successo", - "editInternalResourceDialogError": "Errore", - "editInternalResourceDialogFailedToUpdateInternalResource": "Impossibile aggiornare la risorsa interna", - "editInternalResourceDialogNameRequired": "Il nome è obbligatorio", - "editInternalResourceDialogNameMaxLength": "Il nome deve essere inferiore a 255 caratteri", - "editInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1", - "editInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido", - "editInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1", - "editInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536", - "createInternalResourceDialogNoSitesAvailable": "Nessun Sito Disponibile", - "createInternalResourceDialogNoSitesAvailableDescription": "Devi avere almeno un sito Newt con una subnet configurata per creare risorse interne.", - "createInternalResourceDialogClose": "Chiudi", - "createInternalResourceDialogCreateClientResource": "Crea Risorsa Client", - "createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile ai client connessi al sito selezionato.", - "createInternalResourceDialogResourceProperties": "Proprietà della Risorsa", - "createInternalResourceDialogName": "Nome", - "createInternalResourceDialogSite": "Sito", - "createInternalResourceDialogSelectSite": "Seleziona sito...", - "createInternalResourceDialogSearchSites": "Cerca siti...", - "createInternalResourceDialogNoSitesFound": "Nessun sito trovato.", - "createInternalResourceDialogProtocol": "Protocollo", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Porta del Sito", - "createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.", - "createInternalResourceDialogTargetConfiguration": "Configurazione Target", - "createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP o hostname della risorsa nella rete del sito.", - "createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.", - "createInternalResourceDialogCancel": "Annulla", - "createInternalResourceDialogCreateResource": "Crea Risorsa", - "createInternalResourceDialogSuccess": "Successo", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Risorsa interna creata con successo", - "createInternalResourceDialogError": "Errore", - "createInternalResourceDialogFailedToCreateInternalResource": "Impossibile creare la risorsa interna", - "createInternalResourceDialogNameRequired": "Il nome è obbligatorio", - "createInternalResourceDialogNameMaxLength": "Il nome non deve superare i 255 caratteri", - "createInternalResourceDialogPleaseSelectSite": "Si prega di selezionare un sito", - "createInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1", - "createInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido", - "createInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1", - "createInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536", - "siteConfiguration": "Configurazione", - "siteAcceptClientConnections": "Accetta Connessioni Client", - "siteAcceptClientConnectionsDescription": "Permetti ad altri dispositivi di connettersi attraverso questa istanza Newt come gateway utilizzando i client.", - "siteAddress": "Indirizzo del Sito", - "siteAddressDescription": "Specifica l'indirizzo IP dell'host a cui i client si collegano. Questo è l'indirizzo interno del sito nella rete Pangolin per indirizzare i client. Deve rientrare nella subnet dell'Organizzazione.", - "autoLoginExternalIdp": "Accesso Automatico con IDP Esterno", - "autoLoginExternalIdpDescription": "Reindirizzare immediatamente l'utente all'IDP esterno per l'autenticazione.", - "selectIdp": "Seleziona IDP", - "selectIdpPlaceholder": "Scegli un IDP...", - "selectIdpRequired": "Si prega di selezionare un IDP quando l'accesso automatico è abilitato.", - "autoLoginTitle": "Reindirizzamento", - "autoLoginDescription": "Reindirizzandoti al provider di identità esterno per l'autenticazione.", - "autoLoginProcessing": "Preparazione dell'autenticazione...", - "autoLoginRedirecting": "Reindirizzamento al login...", - "autoLoginError": "Errore di Accesso Automatico", - "autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.", - "autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.", - "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", - "introTitle": "Managed Self-Hosted Pangolin", - "introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.", - "introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin — i tunnel, la terminazione SSL e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:", - "benefitSimplerOperations": { - "title": "Operazioni più semplici", - "description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella." - }, - "benefitAutomaticUpdates": { - "title": "Aggiornamenti automatici", - "description": "Il cruscotto cloud si evolve rapidamente, in modo da ottenere nuove funzionalità e correzioni di bug senza dover tirare manualmente nuovi contenitori ogni volta." - }, - "benefitLessMaintenance": { - "title": "Meno manutenzione", - "description": "Nessuna migrazione di database, backup o infrastruttura extra da gestire. Gestiamo questo problema nel cloud." - }, - "benefitCloudFailover": { - "title": "failover del cloud", - "description": "Se il tuo nodo scende, i tuoi tunnel possono temporaneamente fallire nei nostri punti di presenza cloud fino a quando non lo riporti online." - }, - "benefitHighAvailability": { - "title": "Alta disponibilità (PoPs)", - "description": "Puoi anche allegare più nodi al tuo account per ridondanza e prestazioni migliori." - }, - "benefitFutureEnhancements": { - "title": "Miglioramenti futuri", - "description": "Stiamo pianificando di aggiungere più strumenti di analisi, allerta e gestione per rendere la tua distribuzione ancora più robusta." - }, - "docsAlert": { - "text": "Scopri di più sull'opzione Managed Self-Hosted nella nostra", - "documentation": "documentazione" - }, - "convertButton": "Converti questo nodo in auto-ospitato gestito" - }, - "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", - "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", - "domainPickerUnverified": "Non Verificato", - "domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.", - "domainPickerError": "Errore", - "domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione", - "domainPickerErrorCheckAvailability": "Impossibile verificare la disponibilità del dominio", - "domainPickerInvalidSubdomain": "Sottodominio non valido", - "domainPickerInvalidSubdomainRemoved": "L'input \"{sub}\" è stato rimosso perché non è valido.", - "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", - "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 deleted file mode 100644 index a1f2d451..00000000 --- a/messages/ko-KR.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", - "setupNewOrg": "새 조직", - "setupCreateOrg": "조직 생성", - "setupCreateResources": "리소스 생성", - "setupOrgName": "조직 이름", - "orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.", - "orgId": "조직 ID", - "setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.", - "setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.", - "componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.", - "componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.", - "welcome": "판골린에 오신 것을 환영합니다.", - "welcomeTo": "환영합니다", - "componentsCreateOrg": "조직 생성", - "componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.", - "componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", - "dismiss": "해제", - "componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", - "componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!", - "inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.", - "inviteErrorUser": "죄송하지만, 접근하려는 초대가 이 사용자에게 해당되지 않는 것 같습니다.", - "inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.", - "inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.", - "inviteCreateUser": "먼저 계정을 생성해 주세요.", - "goHome": "홈으로 가기", - "inviteLogInOtherUser": "다른 사용자로 로그인", - "createAnAccount": "계정 만들기", - "inviteNotAccepted": "초대가 수락되지 않음", - "authCreateAccount": "시작하려면 계정을 생성하세요.", - "authNoAccount": "계정이 없으신가요?", - "email": "이메일", - "password": "비밀번호", - "confirmPassword": "비밀번호 확인", - "createAccount": "계정 생성", - "viewSettings": "설정 보기", - "delete": "삭제", - "name": "이름", - "online": "온라인", - "offline": "오프라인", - "site": "사이트", - "dataIn": "데이터 입력", - "dataOut": "데이터 출력", - "connectionType": "연결 유형", - "tunnelType": "터널 유형", - "local": "로컬", - "edit": "편집", - "siteConfirmDelete": "사이트 삭제 확인", - "siteDelete": "사이트 삭제", - "siteMessageRemove": "제거되면 사이트에 더 이상 접근할 수 없습니다. 사이트와 관련된 모든 리소스와 대상도 제거됩니다.", - "siteMessageConfirm": "확인을 위해 아래에 사이트 이름을 입력해 주세요.", - "siteQuestionRemove": "조직에서 사이트 {selectedSite}를 제거하시겠습니까?", - "siteManageSites": "사이트 관리", - "siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용", - "siteCreate": "사이트 생성", - "siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오", - "siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.", - "close": "닫기", - "siteErrorCreate": "사이트 생성 오류", - "siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다", - "siteErrorCreateDefaults": "사이트 기본값을 찾을 수 없습니다", - "method": "방법", - "siteMethodDescription": "이것이 연결을 노출하는 방법입니다.", - "siteLearnNewt": "시스템에 Newt 설치하는 방법 배우기", - "siteSeeConfigOnce": "구성을 한 번만 볼 수 있습니다.", - "siteLoadWGConfig": "WireGuard 구성 로딩 중...", - "siteDocker": "Docker 배포 세부정보 확장", - "toggle": "전환", - "dockerCompose": "도커 컴포즈", - "dockerRun": "도커 실행", - "siteLearnLocal": "로컬 사이트는 터널링하지 않습니다. 자세히 알아보기", - "siteConfirmCopy": "구성을 복사했습니다.", - "searchSitesProgress": "사이트 검색...", - "siteAdd": "사이트 추가", - "siteInstallNewt": "Newt 설치", - "siteInstallNewtDescription": "시스템에서 Newt 실행하기", - "WgConfiguration": "WireGuard 구성", - "WgConfigurationDescription": "네트워크에 연결하기 위한 다음 구성을 사용하십시오.", - "operatingSystem": "운영 체제", - "commands": "명령", - "recommended": "추천", - "siteNewtDescription": "최고의 사용자 경험을 위해 Newt를 사용하십시오. Newt는 WireGuard를 기반으로 하며, 판골린 대시보드 내에서 개인 네트워크의 LAN 주소로 개인 리소스에 접근할 수 있도록 합니다.", - "siteRunsInDocker": "Docker에서 실행", - "siteRunsInShell": "macOS, Linux 및 Windows에서 셸에서 실행", - "siteErrorDelete": "사이트 삭제 오류", - "siteErrorUpdate": "사이트 업데이트에 실패했습니다", - "siteErrorUpdateDescription": "사이트 업데이트 중 오류가 발생했습니다.", - "siteUpdated": "사이트가 업데이트되었습니다", - "siteUpdatedDescription": "사이트가 업데이트되었습니다.", - "siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.", - "siteSettingDescription": "사이트에서 설정을 구성하세요", - "siteSetting": "{siteName} 설정", - "siteNewtTunnel": "뉴트 터널 (추천)", - "siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.", - "siteWg": "기본 WireGuard", - "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", - "siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.", - "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", - "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": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.", - "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": "조직 ID가 누락되었습니다", - "orgMissingMessage": "조직 ID 없이 초대장을 재생성할 수 없습니다.", - "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": "호스트 ID", - "licenseReckeckAll": "모든 키 재확인", - "licenseSiteUsage": "사이트 사용량", - "licenseSiteUsageDecsription": "이 라이센스를 사용하는 사이트 수를 확인하세요.", - "licenseNoSiteLimit": "라이선스가 없는 호스트를 사용하는 사이트 수에 제한이 없습니다.", - "licensePurchase": "라이센스 구매", - "licensePurchaseSites": "추가 사이트 구매", - "licenseSitesUsedMax": "{maxSites}개의 사이트 중 {usedSites}개 사용 중", - "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": "{email}에 대한 초대가 삭제되었습니다.", - "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": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다", - "accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요", - "accessControlsSubmit": "접근 제어 저장", - "roles": "역할", - "accessUsersRoles": "사용자 및 역할 관리", - "accessUsersRolesDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요", - "key": "키", - "createdAt": "생성일", - "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", - "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", - "proxyEnableSSL": "SSL 활성화", - "proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.", - "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": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", - "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 경로를 기준으로 리소스에 대한 액세스를 제어할 수 있습니다. IP 주소 또는 URL 경로를 기준으로 액세스를 허용하거나 거부하는 규칙을 만들 수 있습니다.", - "rulesActions": "작업", - "rulesActionAlwaysAllow": "항상 허용: 모든 인증 방법 우회", - "rulesActionAlwaysDeny": "항상 거부: 모든 요청을 차단합니다. 인증을 시도할 수 없습니다.", - "rulesActionPassToAuth": "인증으로 전달: 인증 방법 시도를 허용합니다", - "rulesMatchCriteria": "일치 기준", - "rulesMatchCriteriaIpAddress": "특정 IP 주소와 일치", - "rulesMatchCriteriaIpAddressRange": "CIDR 표기법으로 IP 주소 범위를 일치시킵니다", - "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": "뉴트 ID", - "newtSecretKey": "Newt 비밀 키", - "architecture": "아키텍처", - "sites": "사이트", - "siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.", - "siteWgCompatibleAllClients": "모든 WireGuard 클라이언트와 호환", - "siteWgManualConfigurationRequired": "수동 구성이 필요합니다.", - "userErrorNotAdminOrOwner": "사용자는 관리자 또는 소유자가 아닙니다.", - "pangolinSettings": "설정 - 판골린", - "accessRoleYour": "귀하의 역할:", - "accessRoleSelect2": "역할 선택", - "accessUserSelect": "사용자를 선택하세요.", - "otpEmailEnter": "이메일을 입력하세요", - "otpEmailEnterDescription": "입력 필드에 입력한 후 Enter 키를 눌러 이메일을 추가합니다.", - "otpEmailErrorInvalid": "유효하지 않은 이메일 주소입니다. 와일드카드(*)는 전체 로컬 부분이어야 합니다.", - "otpEmailSmtpRequired": "SMTP 필요", - "otpEmailSmtpRequiredDescription": "일회성 비밀번호 인증을 사용하려면 서버에서 SMTP가 활성화되어 있어야 합니다.", - "otpEmailTitle": "일회용 비밀번호", - "otpEmailTitleDescription": "리소스 접근을 위한 이메일 기반 인증 필요", - "otpEmailWhitelist": "이메일 화이트리스트", - "otpEmailWhitelistList": "화이트리스트된 이메일", - "otpEmailWhitelistListDescription": "이 이메일 주소를 가진 사용자만 이 리소스에 접근할 수 있습니다. 그들은 이메일로 전송된 일회용 비밀번호를 입력하라는 메시지를 받게 됩니다. 도메인에서 모든 이메일 주소를 허용하기 위해 와일드카드(*@example.com)를 사용할 수 있습니다.", - "otpEmailWhitelistSave": "허용 목록 저장", - "passwordAdd": "비밀번호 추가", - "passwordRemove": "비밀번호 제거", - "pincodeAdd": "PIN 코드 추가", - "pincodeRemove": "PIN 코드 제거", - "resourceAuthMethods": "인증 방법", - "resourceAuthMethodsDescriptions": "추가 인증 방법을 통해 리소스에 대한 액세스 허용", - "resourceAuthSettingsSave": "성공적으로 저장되었습니다.", - "resourceAuthSettingsSaveDescription": "인증 설정이 저장되었습니다", - "resourceErrorAuthFetch": "데이터를 가져오는 데 실패했습니다.", - "resourceErrorAuthFetchDescription": "데이터를 가져오는 중 오류가 발생했습니다.", - "resourceErrorPasswordRemove": "리소스 비밀번호 제거 오류", - "resourceErrorPasswordRemoveDescription": "리소스 비밀번호를 제거하는 동안 오류가 발생했습니다.", - "resourceErrorPasswordSetup": "리소스 비밀번호 설정 오류", - "resourceErrorPasswordSetupDescription": "리소스 비밀번호 설정 중 오류가 발생했습니다", - "resourceErrorPincodeRemove": "리소스 핀 코드 제거 오류", - "resourceErrorPincodeRemoveDescription": "리소스 핀코드를 제거하는 중 오류가 발생했습니다.", - "resourceErrorPincodeSetup": "리소스 PIN 코드 설정 중 오류가 발생했습니다.", - "resourceErrorPincodeSetupDescription": "리소스 PIN 코드를 설정하는 동안 오류가 발생했습니다.", - "resourceErrorUsersRolesSave": "역할 설정에 실패했습니다.", - "resourceErrorUsersRolesSaveDescription": "역할 설정 중 오류가 발생했습니다.", - "resourceErrorWhitelistSave": "화이트리스트 저장에 실패했습니다.", - "resourceErrorWhitelistSaveDescription": "화이트리스트를 저장하는 동안 오류가 발생했습니다.", - "resourcePasswordSubmit": "비밀번호 보호 활성화", - "resourcePasswordProtection": "비밀번호 보호 {status}", - "resourcePasswordRemove": "리소스 비밀번호가 제거되었습니다", - "resourcePasswordRemoveDescription": "리소스 비밀번호가 성공적으로 제거되었습니다.", - "resourcePasswordSetup": "리소스 비밀번호 설정됨", - "resourcePasswordSetupDescription": "리소스 비밀번호가 성공적으로 설정되었습니다.", - "resourcePasswordSetupTitle": "비밀번호 설정", - "resourcePasswordSetupTitleDescription": "이 리소스를 보호하기 위해 비밀번호를 설정하세요.", - "resourcePincode": "PIN 코드", - "resourcePincodeSubmit": "PIN 코드 보호 활성화", - "resourcePincodeProtection": "PIN 코드 보호 {상태}", - "resourcePincodeRemove": "리소스 핀코드가 제거되었습니다.", - "resourcePincodeRemoveDescription": "리소스 비밀번호가 성공적으로 제거되었습니다.", - "resourcePincodeSetup": "리소스 PIN 코드가 설정되었습니다", - "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": "시스템에서 ID 제공자를 보고 관리합니다", - "idpDeletedDescription": "신원 공급자가 성공적으로 삭제되었습니다", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "정말로 아이덴티티 공급자 {name}를 영구적으로 삭제하시겠습니까?", - "idpMessageRemove": "이 작업은 아이덴티티 공급자와 모든 관련 구성을 제거합니다. 이 공급자를 통해 인증하는 사용자는 더 이상 로그인할 수 없습니다.", - "idpMessageConfirm": "확인을 위해 아래에 아이덴티티 제공자의 이름을 입력하세요.", - "idpConfirmDelete": "신원 제공자 삭제 확인", - "idpDelete": "아이덴티티 공급자 삭제", - "idp": "신원 공급자", - "idpSearch": "ID 공급자 검색...", - "idpAdd": "아이덴티티 공급자 추가", - "idpClientIdRequired": "클라이언트 ID가 필요합니다.", - "idpClientSecretRequired": "클라이언트 비밀이 필요합니다.", - "idpErrorAuthUrlInvalid": "인증 URL은 유효한 URL이어야 합니다.", - "idpErrorTokenUrlInvalid": "토큰 URL은 유효한 URL이어야 합니다.", - "idpPathRequired": "식별자 경로가 필요합니다.", - "idpScopeRequired": "범위가 필요합니다.", - "idpOidcDescription": "OpenID Connect ID 공급자를 구성하십시오.", - "idpCreatedDescription": "ID 공급자가 성공적으로 생성되었습니다.", - "idpCreate": "아이덴티티 공급자 생성", - "idpCreateDescription": "사용자 인증을 위한 새로운 ID 공급자를 구성합니다.", - "idpSeeAll": "모든 ID 공급자 보기", - "idpSettingsDescription": "신원 제공자의 기본 정보를 구성하세요", - "idpDisplayName": "이 신원 공급자를 위한 표시 이름", - "idpAutoProvisionUsers": "사용자 자동 프로비저닝", - "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", - "licenseBadge": "EE", - "idpType": "제공자 유형", - "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", - "idpOidcConfigure": "OAuth2/OIDC 구성", - "idpOidcConfigureDescription": "OAuth2/OIDC 공급자 엔드포인트 및 자격 증명을 구성하십시오.", - "idpClientId": "클라이언트 ID", - "idpClientIdDescription": "아이덴티티 공급자의 OAuth2 클라이언트 ID", - "idpClientSecret": "클라이언트 비밀", - "idpClientSecretDescription": "신원 제공자로부터의 OAuth2 클라이언트 비밀", - "idpAuthUrl": "인증 URL", - "idpAuthUrlDescription": "OAuth2 인증 엔드포인트 URL", - "idpTokenUrl": "토큰 URL", - "idpTokenUrlDescription": "OAuth2 토큰 엔드포인트 URL", - "idpOidcConfigureAlert": "중요 정보", - "idpOidcConfigureAlertDescription": "아이덴티티 공급자를 생성한 후, 아이덴티티 공급자의 설정에서 콜백 URL을 구성해야 합니다. 콜백 URL은 성공적으로 생성된 후 제공됩니다.", - "idpToken": "토큰 구성", - "idpTokenDescription": "ID 토큰에서 사용자 정보를 추출하는 방법 구성", - "idpJmespathAbout": "JMESPath에 대하여", - "idpJmespathAboutDescription": "아래 경로는 ID 토큰에서 값을 추출하기 위해 JMESPath 구문을 사용합니다.", - "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": "조직 정책은 사용자의 ID 토큰에 따라 조직에 대한 액세스를 제어하는 데 사용됩니다. ID 토큰에서 역할 및 조직 정보를 추출하기 위해 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": "인증 - 판골린", - "verificationCodeLengthRequirements": "인증 코드가 8자여야 합니다.", - "errorOccurred": "오류가 발생했습니다.", - "emailErrorVerify": "이메일 확인에 실패했습니다:", - "emailVerified": "이메일이 성공적으로 확인되었습니다! 리디렉션 중입니다...", - "verificationCodeErrorResend": "인증 코드를 재전송하는 데 실패했습니다:", - "verificationCodeResend": "인증 코드가 재전송되었습니다", - "verificationCodeResendDescription": "검증 코드를 귀하의 이메일 주소로 재전송했습니다. 받은 편지함을 확인해 주세요.", - "emailVerify": "이메일 확인", - "emailVerifyDescription": "이메일 주소로 전송된 인증 코드를 입력하세요.", - "verificationCode": "인증 코드", - "verificationCodeEmailSent": "귀하의 이메일 주소로 인증 코드가 전송되었습니다.", - "submit": "제출", - "emailVerifyResendProgress": "재전송 중...", - "emailVerifyResend": "코드를 받지 못하셨나요? 여기 클릭하여 재전송하세요", - "passwordNotMatch": "비밀번호가 일치하지 않습니다.", - "signupError": "가입하는 동안 오류가 발생했습니다.", - "pangolinLogoAlt": "판골린 로고", - "inviteAlready": "초대받은 것 같습니다!", - "inviteAlreadyDescription": "초대를 수락하려면 로그인하거나 계정을 생성해야 합니다.", - "signupQuestion": "이미 계정이 있습니까?", - "login": "로그인", - "resourceNotFound": "리소스를 찾을 수 없습니다", - "resourceNotFoundDescription": "접근하려는 리소스가 존재하지 않습니다.", - "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", - "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", - "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자리 PIN 코드", - "pincodeSubmit": "PIN으로 로그인", - "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": "IdP를 찾을 수 없습니다.", - "inviteInvalid": "유효하지 않은 초대", - "inviteInvalidDescription": "초대 링크가 유효하지 않습니다.", - "inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다", - "inviteErrorUserNotExists": "사용자가 존재하지 않습니다. 먼저 계정을 생성해 주세요.", - "inviteErrorLoginRequired": "초대를 수락하려면 로그인해야 합니다.", - "inviteErrorExpired": "초대가 만료되었을 수 있습니다.", - "inviteErrorRevoked": "초대가 취소되었을 수 있습니다.", - "inviteErrorTypo": "초대 링크에 오타가 있을 수 있습니다.", - "pangolinSetup": "설정 - 판골린", - "orgNameRequired": "조직 이름은 필수입니다.", - "orgIdRequired": "조직 ID가 필요합니다", - "orgErrorCreate": "조직 생성 중 오류가 발생했습니다.", - "pageNotFound": "페이지를 찾을 수 없습니다", - "pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.", - "overview": "개요", - "home": "홈", - "accessControl": "액세스 제어", - "settings": "설정", - "usersAll": "모든 사용자", - "license": "라이선스", - "pangolinDashboard": "대시보드 - 판골린", - "noResults": "결과를 찾을 수 없습니다.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "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": "이 기능은 Professional Edition에서만 사용할 수 있습니다.", - "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 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", - "enableDockerSocketLink": "자세히 알아보기", - "viewDockerContainers": "도커 컨테이너 보기", - "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": "판골린 호스팅", - "fossorial": "지하 서식", - "completeAccountSetup": "계정 설정 완료", - "completeAccountSetupDescription": "시작하려면 비밀번호를 설정하세요", - "accountSetupSent": "이 이메일 주소로 계정 설정 코드를 보내드리겠습니다.", - "accountSetupCode": "설정 코드", - "accountSetupCodeDescription": "설정 코드를 확인하기 위해 이메일을 확인하세요.", - "passwordCreate": "비밀번호 생성", - "passwordCreateConfirm": "비밀번호 확인", - "accountSetupSubmit": "설정 코드 전송", - "completeSetup": "설정 완료", - "accountSetupSuccess": "계정 설정이 완료되었습니다! 판골린에 오신 것을 환영합니다!", - "documentation": "문서", - "saveAllSettings": "모든 설정 저장", - "settingsUpdated": "설정이 업데이트되었습니다", - "settingsUpdatedDescription": "모든 설정이 성공적으로 업데이트되었습니다", - "settingsErrorUpdate": "설정 업데이트 실패", - "settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다", - "sidebarCollapse": "줄이기", - "sidebarExpand": "확장하기", - "newtUpdateAvailable": "업데이트 가능", - "newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.", - "domainPickerEnterDomain": "도메인", - "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.", - "domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.", - "domainPickerTabAll": "모두", - "domainPickerTabOrganization": "조직", - "domainPickerTabProvided": "제공 됨", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "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시간 작동하는 것과 같습니다. 사용량 한도에 도달하면 플랜을 업그레이드하거나 사용량을 줄일 때까지 사이트가 연결 해제됩니다. 노드를 사용할 때 시간은 요금이 청구되지 않습니다.", - "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 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.", - "resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다", - "resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요", - "billingPricingCalculatorLink": "가격 계산기", - "signUpTerms": { - "IAgreeToThe": "동의합니다", - "termsOfService": "서비스 약관", - "and": "및", - "privacyPolicy": "개인 정보 보호 정책" - }, - "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 비밀 키", - "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": "헤더는 새 줄로 구분됨: 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": "사이트 포트", - "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 주소를 지정합니다. 이는 클라이언트가 주소를 지정하기 위한 Pangolin 네트워크의 사이트 내부 주소입니다. 조직 서브넷 내에 있어야 합니다.", - "autoLoginExternalIdp": "외부 IDP로 자동 로그인", - "autoLoginExternalIdpDescription": "인증을 위해 외부 IDP로 사용자를 즉시 리디렉션합니다.", - "selectIdp": "IDP 선택", - "selectIdpPlaceholder": "IDP 선택...", - "selectIdpRequired": "자동 로그인이 활성화된 경우 IDP를 선택하십시오.", - "autoLoginTitle": "리디렉션 중", - "autoLoginDescription": "인증을 위해 외부 ID 공급자로 리디렉션 중입니다.", - "autoLoginProcessing": "인증 준비 중...", - "autoLoginRedirecting": "로그인으로 리디렉션 중...", - "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": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함", - "introTitle": "관리 자체 호스팅 팡골린", - "introDescription": "는 자신의 데이터를 프라이빗하고 자체 호스팅을 유지하면서 더 간단하고 추가적인 신뢰성을 원하는 사람들을 위한 배포 옵션입니다.", - "introDetail": "이 옵션을 사용하면 여전히 자신의 팡골린 노드를 운영하고 - 터널, SSL 종료 및 트래픽 모두 서버에 유지됩니다. 차이점은 관리 및 모니터링이 클라우드 대시보드를 통해 처리되어 여러 혜택을 제공합니다.", - "benefitSimplerOperations": { - "title": "더 간단한 운영", - "description": "자체 메일 서버를 운영하거나 복잡한 경고를 설정할 필요가 없습니다. 기본적으로 상태 점검 및 다운타임 경고를 받을 수 있습니다." - }, - "benefitAutomaticUpdates": { - "title": "자동 업데이트", - "description": "클라우드 대시보드는 빠르게 발전하므로 새로운 기능과 버그 수정 사항을 수동으로 새로운 컨테이너를 가져오지 않고도 받을 수 있습니다." - }, - "benefitLessMaintenance": { - "title": "유지보수 감소", - "description": "데이터베이스 마이그레이션, 백업 또는 추가 인프라를 관리할 필요가 없습니다. 저희가 클라우드에서 처리합니다." - }, - "benefitCloudFailover": { - "title": "클라우드 장애 조치", - "description": "노드가 다운되면 터널이 클라우드의 프레즌스 포인트로 임시 전환되어 노드를 다시 온라인으로 가져올 때까지 유지됩니다." - }, - "benefitHighAvailability": { - "title": "고가용성 (PoPs)", - "description": "계정에 여러 노드를 연결하여 이중성과 성능을 향상시킬 수 있습니다." - }, - "benefitFutureEnhancements": { - "title": "향후 개선", - "description": "배포를 더욱 견고하게 만들기 위해 더 많은 분석, 경고, 및 관리 도구를 추가할 계획입니다." - }, - "docsAlert": { - "text": "관리 자체 호스팅 옵션에 대해 더 알아보세요", - "documentation": "문서" - }, - "convertButton": "이 노드를 관리 자체 호스팅으로 변환" - }, - "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 공급자", - "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": "이메일 인증이 필요합니다. 이 단계를 완료하려면 {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 deleted file mode 100644 index ad8eb643..00000000 --- a/messages/nb-NO.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Lag din organisasjon, område og dine ressurser", - "setupNewOrg": "Ny Organisasjon", - "setupCreateOrg": "Opprett organisasjon", - "setupCreateResources": "Opprett ressurser", - "setupOrgName": "Organisasjonsnavn", - "orgDisplayName": "Dette er visningsnavnet til organisasjonen din.", - "orgId": "Organisasjons-ID", - "setupIdentifierMessage": "Dette er den unike identifikator for din organisasjon. Dette er separat fra visningsnavnet.", - "setupErrorIdentifier": "Organisasjons-ID er allerede tatt. Vennligst velg en annen.", - "componentsErrorNoMemberCreate": "Du er for øyeblikket ikke medlem av noen organisasjoner. Lag en organisasjon for å komme i gang.", - "componentsErrorNoMember": "Du er for øyeblikket ikke medlem av noen organisasjoner.", - "welcome": "Velkommen!", - "welcomeTo": "Velkommen til", - "componentsCreateOrg": "Lag en Organisasjon", - "componentsMember": "Du er {count, plural, =0 {ikke medlem av noen organisasjoner} one {medlem av en organisasjon} other {medlem av # organisasjoner}}.", - "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", - "dismiss": "Avvis", - "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", - "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", - "inviteErrorNotValid": "Beklager, men det ser ut som invitasjonen du prøver å bruke ikke har blitt akseptert eller ikke er gyldig lenger.", - "inviteErrorUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til, ikke er for denne brukeren.", - "inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.", - "inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.", - "inviteCreateUser": "Vennligst opprett en konto først.", - "goHome": "Gå hjem", - "inviteLogInOtherUser": "Logg inn som en annen bruker", - "createAnAccount": "Lag konto", - "inviteNotAccepted": "Invitasjonen ikke akseptert", - "authCreateAccount": "Opprett en konto for å komme i gang", - "authNoAccount": "Har du ikke konto?", - "email": "E-post", - "password": "Passord", - "confirmPassword": "Bekreft Passord", - "createAccount": "Opprett Konto", - "viewSettings": "Vis Innstillinger", - "delete": "Slett", - "name": "Navn", - "online": "Online", - "offline": "Frakoblet", - "site": "Område", - "dataIn": "Data Inn", - "dataOut": "Data Ut", - "connectionType": "Tilkoblingstype", - "tunnelType": "Tunneltype", - "local": "Lokal", - "edit": "Rediger", - "siteConfirmDelete": "Bekreft Sletting av Område", - "siteDelete": "Slett Område", - "siteMessageRemove": "Når området slettes, vil det ikke lenger være tilgjengelig. Alle ressurser og mål assosiert med området vil også bli slettet.", - "siteMessageConfirm": "For å bekrefte, vennligst skriv inn navnet i området nedenfor.", - "siteQuestionRemove": "Er du sikker på at du vil slette området {selectedSite} fra organisasjonen?", - "siteManageSites": "Administrer Områder", - "siteDescription": "Tillat tilkobling til nettverket ditt gjennom sikre tunneler", - "siteCreate": "Opprett område", - "siteCreateDescription2": "Følg trinnene nedenfor for å opprette og koble til et nytt område", - "siteCreateDescription": "Opprett et nytt område for å begynne å koble til ressursene dine", - "close": "Lukk", - "siteErrorCreate": "Feil ved oppretting av område", - "siteErrorCreateKeyPair": "Nøkkelpar eller standardinnstillinger for område ikke funnet", - "siteErrorCreateDefaults": "Standardinnstillinger for område ikke funnet", - "method": "Metode", - "siteMethodDescription": "Slik eksponerer du tilkoblinger.", - "siteLearnNewt": "Lær hvordan du installerer Newt på systemet ditt", - "siteSeeConfigOnce": "Du kan kun se konfigurasjonen én gang.", - "siteLoadWGConfig": "Laster WireGuard-konfigurasjon...", - "siteDocker": "Utvid for detaljer om Docker-deployment", - "toggle": "Veksle", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "Lokale områder tunnelerer ikke, lær mer", - "siteConfirmCopy": "Jeg har kopiert konfigurasjonen", - "searchSitesProgress": "Søker i områder...", - "siteAdd": "Legg til område", - "siteInstallNewt": "Installer Newt", - "siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt", - "WgConfiguration": "WireGuard Konfigurasjon", - "WgConfigurationDescription": "Bruk følgende konfigurasjon for å koble til nettverket ditt", - "operatingSystem": "Operativsystem", - "commands": "Kommandoer", - "recommended": "Anbefalt", - "siteNewtDescription": "For den beste brukeropplevelsen, bruk Newt. Den bruker WireGuard i bakgrunnen og lar deg adressere dine private ressurser med deres LAN-adresse på ditt private nettverk fra Pangolin-dashbordet.", - "siteRunsInDocker": "Kjører i Docker", - "siteRunsInShell": "Kjører i skall på macOS, Linux og Windows", - "siteErrorDelete": "Feil ved sletting av området", - "siteErrorUpdate": "Klarte ikke å oppdatere området", - "siteErrorUpdateDescription": "En feil oppstod under oppdatering av området.", - "siteUpdated": "Område oppdatert", - "siteUpdatedDescription": "Området har blitt oppdatert.", - "siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området", - "siteSettingDescription": "Konfigurer innstillingene for området ditt", - "siteSetting": "{siteName} Innstillinger", - "siteNewtTunnel": "Newt Tunnel (Anbefalt)", - "siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.", - "siteWg": "Grunnleggende WireGuard", - "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", - "siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER", - "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", - "siteSeeAll": "Se alle områder", - "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", - "siteNewtCredentials": "Newt påloggingsinformasjon", - "siteNewtCredentialsDescription": "Slik vil Newt autentisere seg mot serveren", - "siteCredentialsSave": "Lagre påloggingsinformasjonen din", - "siteCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "siteInfo": "Områdeinformasjon", - "status": "Status", - "shareTitle": "Administrer delingslenker", - "shareDescription": "Opprett delbare lenker for å gi midlertidig eller permanent tilgang til ressursene dine", - "shareSearch": "Søk delingslenker...", - "shareCreate": "Opprett delingslenke", - "shareErrorDelete": "Klarte ikke å slette lenke", - "shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke", - "shareDeleted": "Lenke slettet", - "shareDeletedDescription": "Lenken har blitt slettet", - "shareTokenDescription": "Din tilgangsnøkkel kan sendes på to måter: som en query parameter eller i request headers. Disse må sendes fra klienten på hver forespørsel for autentisert tilgang.", - "accessToken": "Tilgangsnøkkel", - "usageExamples": "Brukseksempler", - "tokenId": "Token-ID", - "requestHeades": "Request Headers", - "queryParameter": "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", - "shareTokenSecurety": "Hold tilgangsnøkkelen ditt sikkert. Ikke del i offentlig tilgjengelige områder eller klientkode.", - "shareErrorFetchResource": "Klarte ikke å hente ressurser", - "shareErrorFetchResourceDescription": "En feil oppstod under henting av ressursene", - "shareErrorCreate": "Mislyktes med å opprette delingslenke", - "shareErrorCreateDescription": "Det oppsto en feil ved opprettelse av delingslenken", - "shareCreateDescription": "Alle med denne lenken får tilgang til ressursen", - "shareTitleOptional": "Tittel (valgfritt)", - "expireIn": "Utløper om", - "neverExpire": "Utløper aldri", - "shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.", - "shareSeeOnce": "Du får bare se denne lenken én gang. Pass på å kopiere den.", - "shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.", - "shareTokenUsage": "Se tilgangstokenbruk", - "createLink": "Opprett lenke", - "resourcesNotFound": "Ingen ressurser funnet", - "resourceSearch": "Søk i ressurser", - "openMenu": "Åpne meny", - "resource": "Ressurs", - "title": "Tittel", - "created": "Opprettet", - "expires": "Utløper", - "never": "Aldri", - "shareErrorSelectResource": "Vennligst velg en ressurs", - "resourceTitle": "Administrer Ressurser", - "resourceDescription": "Opprett sikre proxyer til dine private applikasjoner", - "resourcesSearch": "Søk i ressurser...", - "resourceAdd": "Legg til ressurs", - "resourceErrorDelte": "Feil ved sletting av ressurs", - "authentication": "Autentisering", - "protected": "Beskyttet", - "notProtected": "Ikke beskyttet", - "resourceMessageRemove": "Når den er fjernet, vil ressursen ikke lenger være tilgjengelig. Alle mål knyttet til ressursen vil også bli fjernet.", - "resourceMessageConfirm": "For å bekrefte, skriv inn navnet på ressursen nedenfor.", - "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressursen {selectedResource} fra organisasjonen?", - "resourceHTTP": "HTTPS-ressurs", - "resourceHTTPDescription": "Proxy-forespørsler til appen din over HTTPS ved bruk av et underdomene eller grunndomene.", - "resourceRaw": "Rå TCP/UDP-ressurs", - "resourceRawDescription": "Proxyer forespørsler til appen din over TCP/UDP ved å bruke et portnummer.", - "resourceCreate": "Opprett ressurs", - "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", - "resourceSeeAll": "Se alle ressurser", - "resourceInfo": "Ressursinformasjon", - "resourceNameDescription": "Dette er visningsnavnet for ressursen.", - "siteSelect": "Velg område", - "siteSearch": "Søk i område", - "siteNotFound": "Ingen område funnet.", - "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", - "resourceHTTPSSettings": "HTTPS-innstillinger", - "resourceHTTPSSettingsDescription": "Konfigurer tilgang til ressursen din over HTTPS", - "domainType": "Domenetype", - "subdomain": "Underdomene", - "baseDomain": "Grunndomene", - "subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.", - "resourceRawSettings": "TCP/UDP-innstillinger", - "resourceRawSettingsDescription": "Konfigurer tilgang til ressursen din over TCP/UDP", - "protocol": "Protokoll", - "protocolSelect": "Velg en protokoll", - "resourcePortNumber": "Portnummer", - "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", - "cancel": "Avbryt", - "resourceConfig": "Konfigurasjonsutdrag", - "resourceConfigDescription": "Kopier og lim inn disse konfigurasjonsutdragene for å sette opp din TCP/UDP-ressurs", - "resourceAddEntrypoints": "Traefik: Legg til inngangspunkter", - "resourceExposePorts": "Gerbil: Eksponer Porter i Docker Compose", - "resourceLearnRaw": "Lær hvordan å konfigurere TCP/UDP-ressurser", - "resourceBack": "Tilbake til ressurser", - "resourceGoTo": "Gå til ressurs", - "resourceDelete": "Slett ressurs", - "resourceDeleteConfirm": "Bekreft sletting av ressurs", - "visibility": "Synlighet", - "enabled": "Aktivert", - "disabled": "Deaktivert", - "general": "Generelt", - "generalSettings": "Generelle innstillinger", - "proxy": "Proxy", - "internal": "Intern", - "rules": "Regler", - "resourceSettingDescription": "Konfigurer innstillingene på ressursen din", - "resourceSetting": "{resourceName} Innstillinger", - "alwaysAllow": "Alltid tillat", - "alwaysDeny": "Alltid avslå", - "passToAuth": "Pass til Autentisering", - "orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger", - "orgGeneralSettings": "Organisasjonsinnstillinger", - "orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon", - "saveGeneralSettings": "Lagre generelle innstillinger", - "saveSettings": "Lagre innstillinger", - "orgDangerZone": "Faresone", - "orgDangerZoneDescription": "Når du sletter denne organisasjonen er det ingen vei tilbake. Vennligst vær sikker.", - "orgDelete": "Slett organisasjon", - "orgDeleteConfirm": "Bekreft Sletting av Organisasjon", - "orgMessageRemove": "Denne handlingen er irreversibel og vil slette alle tilknyttede data.", - "orgMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på organisasjonen nedenfor.", - "orgQuestionRemove": "Er du sikker på at du vil fjerne organisasjonen {selectedOrg}?", - "orgUpdated": "Organisasjon oppdatert", - "orgUpdatedDescription": "Organisasjonen har blitt oppdatert.", - "orgErrorUpdate": "Kunne ikke oppdatere organisasjonen", - "orgErrorUpdateMessage": "En feil oppsto under oppdatering av organisasjonen.", - "orgErrorFetch": "Klarte ikke å hente organisasjoner", - "orgErrorFetchMessage": "Det oppstod en feil under opplisting av organisasjonene dine", - "orgErrorDelete": "Klarte ikke å slette organisasjon", - "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", - "orgDeleted": "Organisasjon slettet", - "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", - "orgMissing": "Organisasjons-ID Mangler", - "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", - "accessUsersManage": "Administrer brukere", - "accessUsersDescription": "Inviter brukere og gi dem roller for å administrere tilgang til organisasjonen din", - "accessUsersSearch": "Søk etter brukere...", - "accessUserCreate": "Opprett bruker", - "accessUserRemove": "Fjern bruker", - "username": "Brukernavn", - "identityProvider": "Identitetsleverandør", - "role": "Rolle", - "nameRequired": "Navn er påkrevd", - "accessRolesManage": "Administrer Roller", - "accessRolesDescription": "Konfigurer roller for å administrere tilgang til organisasjonen din", - "accessRolesSearch": "Søk etter roller...", - "accessRolesAdd": "Legg til rolle", - "accessRoleDelete": "Slett rolle", - "description": "Beskrivelse", - "inviteTitle": "Åpne invitasjoner", - "inviteDescription": "Administrer invitasjonene dine til andre brukere", - "inviteSearch": "Søk i invitasjoner...", - "minutes": "Minutter", - "hours": "Timer", - "days": "Dager", - "weeks": "Uker", - "months": "Måneder", - "years": "År", - "day": "{count, plural, one {en dag} other {# dager}}", - "apiKeysTitle": "API-nøkkel informasjon", - "apiKeysConfirmCopy2": "Du må bekrefte at du har kopiert API-nøkkelen.", - "apiKeysErrorCreate": "Feil ved oppretting av API-nøkkel", - "apiKeysErrorSetPermission": "Feil ved innstilling av tillatelser", - "apiKeysCreate": "Generer API-nøkkel", - "apiKeysCreateDescription": "Generer en ny API-nøkkel for din organisasjon", - "apiKeysGeneralSettings": "Tillatelser", - "apiKeysGeneralSettingsDescription": "Finn ut hva denne API-nøkkelen kan gjøre", - "apiKeysList": "Din API-nøkkel", - "apiKeysSave": "Lagre API-nøkkelen din", - "apiKeysSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "apiKeysInfo": "Din API-nøkkel er:", - "apiKeysConfirmCopy": "Jeg har kopiert API-nøkkelen", - "generate": "Generer", - "done": "Ferdig", - "apiKeysSeeAll": "Se alle API-nøkler", - "apiKeysPermissionsErrorLoadingActions": "Feil ved innlasting av API-nøkkel handlinger", - "apiKeysPermissionsErrorUpdate": "Feil ved innstilling av tillatelser", - "apiKeysPermissionsUpdated": "Tillatelser oppdatert", - "apiKeysPermissionsUpdatedDescription": "Tillatelsene har blitt oppdatert.", - "apiKeysPermissionsGeneralSettings": "Tillatelser", - "apiKeysPermissionsGeneralSettingsDescription": "Bestem hva denne API-nøkkelen kan gjøre", - "apiKeysPermissionsSave": "Lagre tillatelser", - "apiKeysPermissionsTitle": "Tillatelser", - "apiKeys": "API-nøkler", - "searchApiKeys": "Søk API-nøkler", - "apiKeysAdd": "Generer API-nøkkel", - "apiKeysErrorDelete": "Feil under sletting av API-nøkkel", - "apiKeysErrorDeleteMessage": "Feil ved sletting av API-nøkkel", - "apiKeysQuestionRemove": "Er du sikker på at du vil fjerne API-nøkkelen {selectedApiKey} fra organisasjonen?", - "apiKeysMessageRemove": "Når den er fjernet, vil API-nøkkelen ikke lenger kunne brukes.", - "apiKeysMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på API-nøkkelen nedenfor.", - "apiKeysDeleteConfirm": "Bekreft sletting av API-nøkkel", - "apiKeysDelete": "Slett API-nøkkel", - "apiKeysManage": "Administrer API-nøkler", - "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", - "apiKeysSettings": "{apiKeyName} Innstillinger", - "userTitle": "Administrer alle brukere", - "userDescription": "Vis og administrer alle brukere i systemet", - "userAbount": "Om brukeradministrasjon", - "userAbountDescription": "Denne tabellen viser alle rotbrukerobjekter i systemet. Hver bruker kan tilhøre flere organisasjoner. Å fjerne en bruker fra en organisasjon sletter ikke deres rotbrukerobjekt – de vil forbli i systemet. For å fullstendig fjerne en bruker fra systemet, må du slette deres rotbrukerobjekt ved å bruke slett-handlingen i denne tabellen.", - "userServer": "Serverbrukere", - "userSearch": "Søk serverbrukere...", - "userErrorDelete": "Feil ved sletting av bruker", - "userDeleteConfirm": "Bekreft sletting av bruker", - "userDeleteServer": "Slett bruker fra server", - "userMessageRemove": "Brukeren vil bli fjernet fra alle organisasjoner og vil bli fullstendig fjernet fra serveren.", - "userMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", - "userQuestionRemove": "Er du sikker på at du vil slette {selectedUser} permanent fra serveren?", - "licenseKey": "Lisensnøkkel", - "valid": "Gyldig", - "numberOfSites": "Antall områder", - "licenseKeySearch": "Søk lisensnøkler...", - "licenseKeyAdd": "Legg til lisensnøkkel", - "type": "Type", - "licenseKeyRequired": "Lisensnøkkel er påkrevd", - "licenseTermsAgree": "Du må godta lisensvilkårene", - "licenseErrorKeyLoad": "Feil ved lasting av lisensnøkler", - "licenseErrorKeyLoadDescription": "Det oppstod en feil ved lasting av lisensnøkler.", - "licenseErrorKeyDelete": "Kunne ikke slette lisensnøkkel", - "licenseErrorKeyDeleteDescription": "Det oppstod en feil ved sletting av lisensnøkkel.", - "licenseKeyDeleted": "Lisensnøkkel slettet", - "licenseKeyDeletedDescription": "Lisensnøkkelen har blitt slettet.", - "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", - "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", - "licenseAbout": "Om Lisensiering", - "communityEdition": "Fellesskapsutgave", - "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", - "licenseKeyActivated": "Lisensnøkkel aktivert", - "licenseKeyActivatedDescription": "Lisensnøkkelen har blitt vellykket aktivert.", - "licenseErrorKeyRecheck": "En feil oppsto under verifisering av lisensnøkler", - "licenseErrorKeyRecheckDescription": "Det oppstod en feil under verifisering av lisensnøkler.", - "licenseErrorKeyRechecked": "Lisensnøkler verifisert", - "licenseErrorKeyRecheckedDescription": "Alle lisensnøkler er verifisert", - "licenseActivateKey": "Aktiver lisensnøkkel", - "licenseActivateKeyDescription": "Skriv inn en lisensnøkkel for å aktivere den.", - "licenseActivate": "Aktiver lisens", - "licenseAgreement": "Ved å krysse av denne boksen bekrefter du at du har lest og godtar lisensvilkårene som tilsvarer nivået tilknyttet lisensnøkkelen din.", - "fossorialLicense": "Vis Fossorial kommersiell lisens og abonnementsvilkår", - "licenseMessageRemove": "Dette vil fjerne lisensnøkkelen og alle tilknyttede tillatelser gitt av den.", - "licenseMessageConfirm": "For å bekrefte, vennligst skriv inn lisensnøkkelen nedenfor.", - "licenseQuestionRemove": "Er du sikker på at du vil slette lisensnøkkelen {selectedKey} ?", - "licenseKeyDelete": "Slett Lisensnøkkel", - "licenseKeyDeleteConfirm": "Bekreft sletting av lisensnøkkel", - "licenseTitle": "Behandle lisensstatus", - "licenseTitleDescription": "Se og administrer lisensnøkler i systemet", - "licenseHost": "Vertslisens", - "licenseHostDescription": "Behandle hovedlisensnøkkelen for verten.", - "licensedNot": "Ikke lisensiert", - "hostId": "Verts-ID", - "licenseReckeckAll": "Verifiser alle nøkler", - "licenseSiteUsage": "Område Bruk", - "licenseSiteUsageDecsription": "Vis antall områder som bruker denne lisensen.", - "licenseNoSiteLimit": "Det er ingen grense på antall områder som bruker en ulisensiert vert.", - "licensePurchase": "Kjøp lisens", - "licensePurchaseSites": "Kjøp flere områder", - "licenseSitesUsedMax": "{usedSites} av {maxSites} områder brukt", - "licenseSitesUsed": "{count, plural, =0 {ingen områder} one {ett område} other {# områder}} i systemet.", - "licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}", - "licenseFee": "Lisensavgift", - "licensePriceSite": "Pris per område", - "total": "Totalt", - "licenseContinuePayment": "Fortsett til betaling", - "pricingPage": "Pris oversikt", - "pricingPortal": "Se Kjøpsportal", - "licensePricingPage": "For de mest oppdaterte prisene og rabattene, vennligst besøk", - "invite": "Invitasjoner", - "inviteRegenerate": "Regenerer invitasjonen", - "inviteRegenerateDescription": "Tilbakekall tidligere invitasjon og opprette en ny", - "inviteRemove": "Fjern invitasjon", - "inviteRemoveError": "Mislyktes å fjerne invitasjon", - "inviteRemoveErrorDescription": "Det oppstod en feil under fjerning av invitasjonen.", - "inviteRemoved": "Invitasjon fjernet", - "inviteRemovedDescription": "Invitasjonen for {email} er fjernet.", - "inviteQuestionRemove": "Er du sikker på at du vil fjerne invitasjonen {email}?", - "inviteMessageRemove": "Når fjernet, vil denne invitasjonen ikke lenger være gyldig. Du kan alltid invitere brukeren på nytt senere.", - "inviteMessageConfirm": "For å bekrefte, vennligst tast inn invitasjonens e-postadresse nedenfor.", - "inviteQuestionRegenerate": "Er du sikker på at du vil generere invitasjonen på nytt for {email}? Dette vil ugyldiggjøre den forrige invitasjonen.", - "inviteRemoveConfirm": "Bekreft fjerning av invitasjon", - "inviteRegenerated": "Invitasjon fornyet", - "inviteSent": "En ny invitasjon er sendt til {email}.", - "inviteSentEmail": "Send e-postvarsel til brukeren", - "inviteGenerate": "En ny invitasjon er generert for {email}.", - "inviteDuplicateError": "Dupliser invitasjon", - "inviteDuplicateErrorDescription": "En invitasjon for denne brukeren eksisterer allerede.", - "inviteRateLimitError": "Forespørselsgrense overskredet", - "inviteRateLimitErrorDescription": "Du har overskredet grensen på 3 regenerasjoner per time. Prøv igjen senere.", - "inviteRegenerateError": "Kunne ikke regenerere invitasjon", - "inviteRegenerateErrorDescription": "Det oppsto en feil under regenerering av invitasjonen.", - "inviteValidityPeriod": "Gyldighetsperiode", - "inviteValidityPeriodSelect": "Velg gyldighetsperiode", - "inviteRegenerateMessage": "Invitasjonen er generert på nytt. Brukeren må gå til lenken nedenfor for å akseptere invitasjonen.", - "inviteRegenerateButton": "Regenerer", - "expiresAt": "Utløpstidspunkt", - "accessRoleUnknown": "Ukjent rolle", - "placeholder": "Plassholder", - "userErrorOrgRemove": "En feil oppsto under fjerning av bruker", - "userErrorOrgRemoveDescription": "Det oppstod en feil under fjerning av brukeren.", - "userOrgRemoved": "Bruker fjernet", - "userOrgRemovedDescription": "Brukeren {email} er fjernet fra organisasjonen.", - "userQuestionOrgRemove": "Er du sikker på at du vil fjerne {email} fra organisasjonen?", - "userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.", - "userMessageOrgConfirm": "For å bekrefte, vennligst skriv inn navnet på brukeren nedenfor.", - "userRemoveOrgConfirm": "Bekreft fjerning av bruker", - "userRemoveOrg": "Fjern bruker fra organisasjon", - "users": "Brukere", - "accessRoleMember": "Medlem", - "accessRoleOwner": "Eier", - "userConfirmed": "Bekreftet", - "idpNameInternal": "Intern", - "emailInvalid": "Ugyldig e-postadresse", - "inviteValidityDuration": "Vennligst velg en varighet", - "accessRoleSelectPlease": "Vennligst velg en rolle", - "usernameRequired": "Brukernavn er påkrevd", - "idpSelectPlease": "Vennligst velg en identitetsleverandør", - "idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.", - "accessRoleErrorFetch": "En feil oppsto under henting av roller", - "accessRoleErrorFetchDescription": "En feil oppsto under henting av rollene", - "idpErrorFetch": "En feil oppsto under henting av identitetsleverandører", - "idpErrorFetchDescription": "En feil oppsto ved henting av identitetsleverandører", - "userErrorExists": "Bruker eksisterer allerede", - "userErrorExistsDescription": "Denne brukeren er allerede medlem av organisasjonen.", - "inviteError": "Kunne ikke invitere bruker", - "inviteErrorDescription": "En feil oppsto under invitering av brukeren", - "userInvited": "Bruker invitert", - "userInvitedDescription": "Brukeren er vellykket invitert.", - "userErrorCreate": "Kunne ikke opprette bruker", - "userErrorCreateDescription": "Det oppsto en feil under oppretting av brukeren", - "userCreated": "Bruker opprettet", - "userCreatedDescription": "Brukeren har blitt vellykket opprettet.", - "userTypeInternal": "Intern bruker", - "userTypeInternalDescription": "Inviter en bruker til å bli med i organisasjonen din direkte.", - "userTypeExternal": "Ekstern bruker", - "userTypeExternalDescription": "Opprett en bruker med en ekstern identitetsleverandør.", - "accessUserCreateDescription": "Følg stegene under for å opprette en ny bruker", - "userSeeAll": "Se alle brukere", - "userTypeTitle": "Brukertype", - "userTypeDescription": "Bestem hvordan du vil opprette brukeren", - "userSettings": "Brukerinformasjon", - "userSettingsDescription": "Skriv inn detaljene for den nye brukeren", - "inviteEmailSent": "Send invitasjonsepost til bruker", - "inviteValid": "Gyldig for", - "selectDuration": "Velg varighet", - "accessRoleSelect": "Velg rolle", - "inviteEmailSentDescription": "En e-post er sendt til brukeren med tilgangslenken nedenfor. De må åpne lenken for å akseptere invitasjonen.", - "inviteSentDescription": "Brukeren har blitt invitert. De må åpne lenken nedenfor for å godta invitasjonen.", - "inviteExpiresIn": "Invitasjonen utløper om {days, plural, one {en dag} other {# dager}}.", - "idpTitle": "Identitetsleverandør", - "idpSelect": "Velg identitetsleverandøren for den eksterne brukeren", - "idpNotConfigured": "Ingen identitetsleverandører er konfigurert. Vennligst konfigurer en identitetsleverandør før du oppretter eksterne brukere.", - "usernameUniq": "Dette må matche det unike brukernavnet som finnes i den valgte identitetsleverandøren.", - "emailOptional": "E-post (Valgfritt)", - "nameOptional": "Navn (valgfritt)", - "accessControls": "Tilgangskontroller", - "userDescription2": "Administrer innstillingene for denne brukeren", - "accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle", - "accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.", - "userSaved": "Bruker lagret", - "userSavedDescription": "Brukeren har blitt oppdatert.", - "autoProvisioned": "Auto avlyst", - "autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør", - "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", - "accessControlsSubmit": "Lagre tilgangskontroller", - "roles": "Roller", - "accessUsersRoles": "Administrer brukere og roller", - "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen din.", - "key": "Nøkkel", - "createdAt": "Opprettet", - "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", - "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", - "proxyEnableSSL": "Aktiver SSL", - "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", - "siteErrorFetchDescription": "Det oppstod en feil under henting av ressurs", - "targetErrorDuplicate": "Dupliser mål", - "targetErrorDuplicateDescription": "Et mål med disse innstillingene finnes allerede", - "targetWireGuardErrorInvalidIp": "Ugyldig mål-IP", - "targetWireGuardErrorInvalidIpDescription": "Mål-IP må være i områdets undernett.", - "targetsUpdated": "Mål oppdatert", - "targetsUpdatedDescription": "Mål og innstillinger oppdatert vellykket", - "targetsErrorUpdate": "Feilet å oppdatere mål", - "targetsErrorUpdateDescription": "En feil oppsto under oppdatering av mål", - "targetTlsUpdate": "TLS-innstillinger oppdatert", - "targetTlsUpdateDescription": "Dine TLS-innstillinger er oppdatert", - "targetErrorTlsUpdate": "Feilet under oppdatering av TLS-innstillinger", - "targetErrorTlsUpdateDescription": "Det oppstod en feil under oppdatering av TLS-innstillinger", - "proxyUpdated": "Proxy-innstillinger oppdatert", - "proxyUpdatedDescription": "Proxy-innstillingene dine er oppdatert", - "proxyErrorUpdate": "En feil oppsto under oppdatering av proxyinnstillinger", - "proxyErrorUpdateDescription": "En feil oppsto under oppdatering av proxyinnstillinger", - "targetAddr": "IP / vertsnavn", - "targetPort": "Port", - "targetProtocol": "Protokoll", - "targetTlsSettings": "Sikker tilkoblings-konfigurasjon", - "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", - "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", - "targetTlsSni": "TLS servernavn", - "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", - "targetTlsSubmit": "Lagre innstillinger", - "targets": "Målkonfigurasjon", - "targetsDescription": "Sett opp mål for å rute trafikk til dine backend-tjenester", - "targetStickySessions": "Aktiver klebrige sesjoner", - "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", - "methodSelect": "Velg metode", - "targetSubmit": "Legg til mål", - "targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.", - "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", - "proxyCustomHeaderDescription": "Verts-header som skal settes ved videresending av forespørsler. La stå tom for å bruke standardinnstillingen.", - "proxyAdditionalSubmit": "Lagre proxy-innstillinger", - "subnetMaskErrorInvalid": "Ugyldig subnettmaske. Må være mellom 0 og 32.", - "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat", - "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet", - "path": "Sti", - "matchPath": "Match sti", - "ipAddressRange": "IP-område", - "rulesErrorFetch": "Klarte ikke å hente regler", - "rulesErrorFetchDescription": "Det oppsto en feil under henting av regler", - "rulesErrorDuplicate": "Duplisert regel", - "rulesErrorDuplicateDescription": "En regel med disse innstillingene finnes allerede", - "rulesErrorInvalidIpAddressRange": "Ugyldig CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Vennligst skriv inn en gyldig CIDR-verdi", - "rulesErrorInvalidUrl": "Ugyldig URL-sti", - "rulesErrorInvalidUrlDescription": "Skriv inn en gyldig verdi for URL-sti", - "rulesErrorInvalidIpAddress": "Ugyldig IP", - "rulesErrorInvalidIpAddressDescription": "Skriv inn en gyldig IP-adresse", - "rulesErrorUpdate": "Kunne ikke oppdatere regler", - "rulesErrorUpdateDescription": "Det oppsto en feil under oppdatering av regler", - "rulesUpdated": "Aktiver Regler", - "rulesUpdatedDescription": "Regelevalueringen har blitt oppdatert", - "rulesMatchIpAddressRangeDescription": "Angi en adresse i CIDR-format (f.eks., 103.21.244.0/22)", - "rulesMatchIpAddress": "Angi en IP-adresse (f.eks. 103.21.244.12)", - "rulesMatchUrl": "Skriv inn en URL-sti eller et mønster (f.eks. /api/v1/todos eller /api/v1/*)", - "rulesErrorInvalidPriority": "Ugyldig prioritet", - "rulesErrorInvalidPriorityDescription": "Vennligst skriv inn en gyldig prioritet", - "rulesErrorDuplicatePriority": "Dupliserte prioriteringer", - "rulesErrorDuplicatePriorityDescription": "Vennligst angi unike prioriteringer", - "ruleUpdated": "Regler oppdatert", - "ruleUpdatedDescription": "Reglene er oppdatert", - "ruleErrorUpdate": "Operasjon mislyktes", - "ruleErrorUpdateDescription": "En feil oppsto under lagringsoperasjonen", - "rulesPriority": "Prioritet", - "rulesAction": "Handling", - "rulesMatchType": "Trefftype", - "value": "Verdi", - "rulesAbout": "Om regler", - "rulesAboutDescription": "Regler lar deg kontrollere tilgang til din ressurs basert på et sett med kriterier. Du kan opprette regler for å tillate eller nekte tilgang basert på IP-adresse eller URL-sti.", - "rulesActions": "Handlinger", - "rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder", - "rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes", - "rulesActionPassToAuth": "Pass til Autentisering: Tillat at autentiseringsmetoder forsøkes", - "rulesMatchCriteria": "Samsvarende kriterier", - "rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse", - "rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon", - "rulesMatchCriteriaUrl": "Match en URL-sti eller et mønster", - "rulesEnable": "Aktiver regler", - "rulesEnableDescription": "Aktiver eller deaktiver regelvurdering for denne ressursen", - "rulesResource": "Konfigurasjon av ressursregler", - "rulesResourceDescription": "Konfigurere regler for tilgangskontroll til ressursen din", - "ruleSubmit": "Legg til regel", - "rulesNoOne": "Ingen regler. Legg til en regel ved å bruke skjemaet.", - "rulesOrder": "Regler evalueres etter prioritet i stigende rekkefølge.", - "rulesSubmit": "Lagre regler", - "resourceErrorCreate": "Feil under oppretting av ressurs", - "resourceErrorCreateDescription": "Det oppstod en feil under oppretting av ressursen", - "resourceErrorCreateMessage": "Feil ved oppretting av ressurs:", - "resourceErrorCreateMessageDescription": "En uventet feil oppstod", - "sitesErrorFetch": "Feil ved henting av områder", - "sitesErrorFetchDescription": "En feil oppstod ved henting av områdene", - "domainsErrorFetch": "Kunne ikke hente domener", - "domainsErrorFetchDescription": "Det oppsto en feil under henting av domenene", - "none": "Ingen", - "unknown": "Ukjent", - "resources": "Ressurser", - "resourcesDescription": "Ressurser er proxyer for applikasjoner som kjører på ditt private nettverk. Opprett en ressurs for enhver HTTP/HTTPS- eller rå TCP/UDP-tjeneste på ditt private nettverk. Hver ressurs må kobles til et område for å muliggjøre privat, sikker tilkobling gjennom en kryptert WireGuard-tunnel.", - "resourcesWireGuardConnect": "Sikker tilkobling med WireGuard-kryptering", - "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentiseringsmetoder", - "resourcesUsersRolesAccess": "Bruker- og rollebasert tilgangskontroll", - "resourcesErrorUpdate": "Feilet å slå av/på ressurs", - "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", - "access": "Tilgang", - "shareLink": "{resource} Del Lenke", - "resourceSelect": "Velg ressurs", - "shareLinks": "Del lenker", - "share": "Delbare lenker", - "shareDescription2": "Opprett delbare lenker til ressursene dine. Lenker gir midlertidig eller ubegrenset tilgang til ressursen din. Du kan konfigurere utløpsvarigheten for lenken når du oppretter den.", - "shareEasyCreate": "Enkelt å lage og dele", - "shareConfigurableExpirationDuration": "Konfigurerbar utløpsvarighet", - "shareSecureAndRevocable": "Sikker og tilbakekallbar", - "nameMin": "Navn må være minst {len} tegn.", - "nameMax": "Navn kan ikke være lengre enn {len} tegn.", - "sitesConfirmCopy": "Vennligst bekreft at du har kopiert konfigurasjonen.", - "unknownCommand": "Ukjent kommando", - "newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}", - "newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}", - "newtEndpoint": "Newt endepunkt", - "newtId": "Newt-ID", - "newtSecretKey": "Newt hemmelig nøkkel", - "architecture": "Arkitektur", - "sites": "Områder", - "siteWgAnyClients": "Bruk en hvilken som helst WireGuard-klient for å koble til. Du må adressere dine interne ressurser ved å bruke peer-IP-en.", - "siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter", - "siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd", - "userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier", - "pangolinSettings": "Innstillinger - Pangolin", - "accessRoleYour": "Din rolle:", - "accessRoleSelect2": "Velg en rolle", - "accessUserSelect": "Velg en bruker", - "otpEmailEnter": "Skriv inn én e-post", - "otpEmailEnterDescription": "Trykk enter for å legge til en e-post etter å ha tastet den inn i tekstfeltet.", - "otpEmailErrorInvalid": "Ugyldig e-postadresse. Jokertegnet (*) må være hele lokaldelen.", - "otpEmailSmtpRequired": "SMTP påkrevd", - "otpEmailSmtpRequiredDescription": "SMTP må være aktivert på serveren for å bruke engangspassord-autentisering.", - "otpEmailTitle": "Engangspassord", - "otpEmailTitleDescription": "Krev e-postbasert autentisering for ressurstilgang", - "otpEmailWhitelist": "E-post-hviteliste", - "otpEmailWhitelistList": "Hvitlistede e-poster", - "otpEmailWhitelistListDescription": "Kun brukere med disse e-postadressene vil ha tilgang til denne ressursen. De vil bli bedt om å skrive inn et engangspassord sendt til e-posten deres. Jokertegn (*@example.com) kan brukes for å tillate enhver e-postadresse fra et domene.", - "otpEmailWhitelistSave": "Lagre hvitliste", - "passwordAdd": "Legg til passord", - "passwordRemove": "Fjern passord", - "pincodeAdd": "Legg til PIN-kode", - "pincodeRemove": "Fjern PIN-kode", - "resourceAuthMethods": "Autentiseringsmetoder", - "resourceAuthMethodsDescriptions": "Tillat tilgang til ressursen via ytterligere autentiseringsmetoder", - "resourceAuthSettingsSave": "Lagret vellykket", - "resourceAuthSettingsSaveDescription": "Autentiseringsinnstillinger er lagret", - "resourceErrorAuthFetch": "Kunne ikke hente data", - "resourceErrorAuthFetchDescription": "Det oppstod en feil ved henting av data", - "resourceErrorPasswordRemove": "Feil ved fjerning av passord for ressurs", - "resourceErrorPasswordRemoveDescription": "Det oppstod en feil ved fjerning av ressurspassordet", - "resourceErrorPasswordSetup": "Feil ved innstilling av ressurspassord", - "resourceErrorPasswordSetupDescription": "Det oppstod en feil ved innstilling av ressurspassordet", - "resourceErrorPincodeRemove": "Feil ved fjerning av ressurs-PIN-koden", - "resourceErrorPincodeRemoveDescription": "Det oppstod en feil under fjerning av ressurs-pinkoden", - "resourceErrorPincodeSetup": "Feil ved innstilling av ressurs-PIN-kode", - "resourceErrorPincodeSetupDescription": "Det oppstod en feil under innstilling av ressursens PIN-kode", - "resourceErrorUsersRolesSave": "Klarte ikke å sette roller", - "resourceErrorUsersRolesSaveDescription": "En feil oppstod ved innstilling av rollene", - "resourceErrorWhitelistSave": "Feilet å lagre hvitliste", - "resourceErrorWhitelistSaveDescription": "Det oppstod en feil under lagring av hvitlisten", - "resourcePasswordSubmit": "Aktiver passordbeskyttelse", - "resourcePasswordProtection": "Passordbeskyttelse {status}", - "resourcePasswordRemove": "Ressurspassord fjernet", - "resourcePasswordRemoveDescription": "Fjerning av ressurspassordet var vellykket", - "resourcePasswordSetup": "Ressurspassord satt", - "resourcePasswordSetupDescription": "Ressurspassordet har blitt vellykket satt", - "resourcePasswordSetupTitle": "Angi passord", - "resourcePasswordSetupTitleDescription": "Sett et passord for å beskytte denne ressursen", - "resourcePincode": "PIN-kode", - "resourcePincodeSubmit": "Aktiver PIN-kodebeskyttelse", - "resourcePincodeProtection": "PIN-kodebeskyttelse {status}", - "resourcePincodeRemove": "Ressurs PIN-kode fjernet", - "resourcePincodeRemoveDescription": "Ressurspassordet ble fjernet", - "resourcePincodeSetup": "Ressurs PIN-kode satt", - "resourcePincodeSetupDescription": "Ressurs PIN-kode er satt vellykket", - "resourcePincodeSetupTitle": "Angi PIN-kode", - "resourcePincodeSetupTitleDescription": "Sett en pinkode for å beskytte denne ressursen", - "resourceRoleDescription": "Administratorer har alltid tilgang til denne ressursen.", - "resourceUsersRoles": "Brukere og Roller", - "resourceUsersRolesDescription": "Konfigurer hvilke brukere og roller som har tilgang til denne ressursen", - "resourceUsersRolesSubmit": "Lagre brukere og roller", - "resourceWhitelistSave": "Lagring vellykket", - "resourceWhitelistSaveDescription": "Hvitlisteinnstillinger er lagret", - "ssoUse": "Bruk plattform SSO", - "ssoUseDescription": "Eksisterende brukere trenger kun å logge på én gang for alle ressurser som har dette aktivert.", - "proxyErrorInvalidPort": "Ugyldig portnummer", - "subdomainErrorInvalid": "Ugyldig underdomene", - "domainErrorFetch": "Feil ved henting av domener", - "domainErrorFetchDescription": "Det oppstod en feil ved henting av domenene", - "resourceErrorUpdate": "Mislyktes å oppdatere ressurs", - "resourceErrorUpdateDescription": "Det oppstod en feil under oppdatering av ressursen", - "resourceUpdated": "Ressurs oppdatert", - "resourceUpdatedDescription": "Ressursen er oppdatert vellykket", - "resourceErrorTransfer": "Klarte ikke å overføre ressurs", - "resourceErrorTransferDescription": "En feil oppsto under overføring av ressursen", - "resourceTransferred": "Ressurs overført", - "resourceTransferredDescription": "Ressursen er overført vellykket.", - "resourceErrorToggle": "Feilet å veksle ressurs", - "resourceErrorToggleDescription": "Det oppstod en feil ved oppdatering av ressursen", - "resourceVisibilityTitle": "Synlighet", - "resourceVisibilityTitleDescription": "Fullstendig aktiver eller deaktiver ressursynlighet", - "resourceGeneral": "Generelle innstillinger", - "resourceGeneralDescription": "Konfigurer de generelle innstillingene for denne ressursen", - "resourceEnable": "Aktiver ressurs", - "resourceTransfer": "Overfør Ressurs", - "resourceTransferDescription": "Overfør denne ressursen til et annet område", - "resourceTransferSubmit": "Overfør ressurs", - "siteDestination": "Destinasjonsområde", - "searchSites": "Søk områder", - "accessRoleCreate": "Opprett rolle", - "accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.", - "accessRoleCreateSubmit": "Opprett rolle", - "accessRoleCreated": "Rolle opprettet", - "accessRoleCreatedDescription": "Rollen er vellykket opprettet.", - "accessRoleErrorCreate": "Klarte ikke å opprette rolle", - "accessRoleErrorCreateDescription": "Det oppstod en feil under opprettelse av rollen.", - "accessRoleErrorNewRequired": "Ny rolle kreves", - "accessRoleErrorRemove": "Kunne ikke fjerne rolle", - "accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.", - "accessRoleName": "Rollenavn", - "accessRoleQuestionRemove": "Du er i ferd med å slette rollen {name}. Du kan ikke angre denne handlingen.", - "accessRoleRemove": "Fjern Rolle", - "accessRoleRemoveDescription": "Fjern en rolle fra organisasjonen", - "accessRoleRemoveSubmit": "Fjern Rolle", - "accessRoleRemoved": "Rolle fjernet", - "accessRoleRemovedDescription": "Rollen er vellykket fjernet.", - "accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.", - "manage": "Administrer", - "sitesNotFound": "Ingen områder funnet.", - "pangolinServerAdmin": "Server Admin - Pangolin", - "licenseTierProfessional": "Profesjonell lisens", - "licenseTierEnterprise": "Bedriftslisens", - "licenseTierPersonal": "Personal License", - "licensed": "Lisensiert", - "yes": "Ja", - "no": "Nei", - "sitesAdditional": "Ytterligere områder", - "licenseKeys": "Lisensnøkler", - "sitestCountDecrease": "Reduser antall områder", - "sitestCountIncrease": "Øk antall områder", - "idpManage": "Administrer Identitetsleverandører", - "idpManageDescription": "Vis og administrer identitetsleverandører i systemet", - "idpDeletedDescription": "Identitetsleverandør slettet vellykket", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren {name} permanent?", - "idpMessageRemove": "Dette vil fjerne identitetsleverandøren og alle tilhørende konfigurasjoner. Brukere som autentiserer seg via denne leverandøren vil ikke lenger kunne logge inn.", - "idpMessageConfirm": "For å bekrefte, vennligst skriv inn navnet på identitetsleverandøren nedenfor.", - "idpConfirmDelete": "Bekreft Sletting av Identitetsleverandør", - "idpDelete": "Slett identitetsleverandør", - "idp": "Identitetsleverandører", - "idpSearch": "Søk identitetsleverandører...", - "idpAdd": "Legg til Identitetsleverandør", - "idpClientIdRequired": "Klient-ID er påkrevd.", - "idpClientSecretRequired": "Klienthemmelighet er påkrevd.", - "idpErrorAuthUrlInvalid": "Autentiserings-URL må være en gyldig URL.", - "idpErrorTokenUrlInvalid": "Token-URL må være en gyldig URL.", - "idpPathRequired": "Identifikatorbane er påkrevd.", - "idpScopeRequired": "Omfang kreves.", - "idpOidcDescription": "Konfigurer en OpenID Connect identitetsleverandør", - "idpCreatedDescription": "Identitetsleverandør opprettet vellykket.", - "idpCreate": "Opprett identitetsleverandør", - "idpCreateDescription": "Konfigurer en ny identitetsleverandør for brukerautentisering", - "idpSeeAll": "Se alle identitetsleverandører", - "idpSettingsDescription": "Konfigurer grunnleggende informasjon for din identitetsleverandør", - "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", - "idpAutoProvisionUsers": "Automatisk brukerklargjøring", - "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", - "licenseBadge": "EE", - "idpType": "Leverandørtype", - "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", - "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", - "idpOidcConfigureDescription": "Konfigurer OAuth2/OIDC-leverandørens endepunkter og legitimasjon", - "idpClientId": "Klient-ID", - "idpClientIdDescription": "OAuth2-klient-ID-en fra identitetsleverandøren din", - "idpClientSecret": "Klienthemmelighet", - "idpClientSecretDescription": "OAuth2-klienthemmeligheten fra din identitetsleverandør", - "idpAuthUrl": "Autorisasjons-URL", - "idpAuthUrlDescription": "OAuth2 autorisasjonsendepunkt URL", - "idpTokenUrl": "Token-URL", - "idpTokenUrlDescription": "OAuth2-tokenendepunkt-URL", - "idpOidcConfigureAlert": "Viktig informasjon", - "idpOidcConfigureAlertDescription": "Etter at du har opprettet identitetsleverandøren, må du konfigurere callback-URL-en i identitetsleverandørens innstillinger. Callback-URL-en blir oppgitt etter vellykket opprettelse.", - "idpToken": "Token-konfigurasjon", - "idpTokenDescription": "Konfigurer hvordan brukerinformasjon trekkes ut fra ID-tokenet", - "idpJmespathAbout": "Om JMESPath", - "idpJmespathAboutDescription": "Stiene nedenfor bruker JMESPath-syntaks for å hente ut verdier fra ID-tokenet.", - "idpJmespathAboutDescriptionLink": "Lær mer om JMESPath", - "idpJmespathLabel": "Identifikatorsti", - "idpJmespathLabelDescription": "Stien til brukeridentifikatoren i ID-tokenet", - "idpJmespathEmailPathOptional": "E-poststi (Valgfritt)", - "idpJmespathEmailPathOptionalDescription": "Stien til brukerens e-postadresse i ID-tokenet", - "idpJmespathNamePathOptional": "Navn Sti (Valgfritt)", - "idpJmespathNamePathOptionalDescription": "Stien til brukerens navn i ID-tokenet", - "idpOidcConfigureScopes": "Omfang", - "idpOidcConfigureScopesDescription": "Mellomromseparert liste over OAuth2-omfang å be om", - "idpSubmit": "Opprett identitetsleverandør", - "orgPolicies": "Organisasjonsretningslinjer", - "idpSettings": "{idpName} Innstillinger", - "idpCreateSettingsDescription": "Konfigurer innstillingene for din identitetsleverandør", - "roleMapping": "Rolletilordning", - "orgMapping": "Organisasjon Kartlegging", - "orgPoliciesSearch": "Søk i organisasjonens retningslinjer...", - "orgPoliciesAdd": "Legg til organisasjonspolicy", - "orgRequired": "Organisasjon er påkrevd", - "error": "Feil", - "success": "Suksess", - "orgPolicyAddedDescription": "Policy vellykket lagt til", - "orgPolicyUpdatedDescription": "Policyen er vellykket oppdatert", - "orgPolicyDeletedDescription": "Policy slettet vellykket", - "defaultMappingsUpdatedDescription": "Standardtilordninger oppdatert vellykket", - "orgPoliciesAbout": "Om organisasjonens retningslinjer", - "orgPoliciesAboutDescription": "Organisasjonspolicyer brukes til å kontrollere tilgang til organisasjoner basert på brukerens ID-token. Du kan spesifisere JMESPath-uttrykk for å trekke ut rolle- og organisasjonsinformasjon fra ID-tokenet.", - "orgPoliciesAboutDescriptionLink": "Se dokumentasjon, for mer informasjon.", - "defaultMappingsOptional": "Standard Tilordninger (Valgfritt)", - "defaultMappingsOptionalDescription": "Standardtilordningene brukes når det ikke er definert en organisasjonspolicy for en organisasjon. Du kan spesifisere standard rolle- og organisasjonstilordninger som det kan falles tilbake på her.", - "defaultMappingsRole": "Standard rolletilordning", - "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", - "defaultMappingsOrg": "Standard organisasjonstilordning", - "defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.", - "defaultMappingsSubmit": "Lagre standard tilordninger", - "orgPoliciesEdit": "Rediger Organisasjonspolicy", - "org": "Organisasjon", - "orgSelect": "Velg organisasjon", - "orgSearch": "Søk organisasjon", - "orgNotFound": "Ingen organisasjon funnet.", - "roleMappingPathOptional": "Rollekartleggingssti (Valgfritt)", - "orgMappingPathOptional": "Organisasjonstilordningssti (Valgfritt)", - "orgPolicyUpdate": "Oppdater policy", - "orgPolicyAdd": "Legg til policy", - "orgPolicyConfig": "Konfigurer tilgang for en organisasjon", - "idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert", - "redirectUrl": "Omdirigerings-URL", - "redirectUrlAbout": "Om omdirigerings-URL", - "redirectUrlAboutDescription": "Dette er URL-en som brukere vil bli omdirigert til etter autentisering. Du må konfigurere denne URL-en i innstillingene for identitetsleverandøren din.", - "pangolinAuth": "Autentisering - Pangolin", - "verificationCodeLengthRequirements": "Din verifiseringskode må være 8 tegn.", - "errorOccurred": "Det oppstod en feil", - "emailErrorVerify": "Kunne ikke verifisere e-post:", - "emailVerified": "E-posten er bekreftet! Omdirigerer deg...", - "verificationCodeErrorResend": "Kunne ikke sende bekreftelseskode på nytt:", - "verificationCodeResend": "Bekreftelseskode sendt på nytt", - "verificationCodeResendDescription": "Vi har sendt en ny bekreftelseskode til e-postadressen din. Vennligst sjekk innboksen din.", - "emailVerify": "Verifiser e-post", - "emailVerifyDescription": "Skriv inn bekreftelseskoden sendt til e-postadressen din.", - "verificationCode": "Verifiseringskode", - "verificationCodeEmailSent": "Vi har sendt en bekreftelseskode til e-postadressen din.", - "submit": "Send inn", - "emailVerifyResendProgress": "Sender på nytt...", - "emailVerifyResend": "Har du ikke mottatt en kode? Klikk her for å sende på nytt", - "passwordNotMatch": "Passordene stemmer ikke", - "signupError": "Det oppsto en feil ved registrering", - "pangolinLogoAlt": "Pangolin Logo", - "inviteAlready": "Ser ut til at du har blitt invitert!", - "inviteAlreadyDescription": "For å godta invitasjonen, må du logge inn eller opprette en konto.", - "signupQuestion": "Har du allerede en konto?", - "login": "Logg inn", - "resourceNotFound": "Ressurs ikke funnet", - "resourceNotFoundDescription": "Ressursen du prøver å få tilgang til eksisterer ikke.", - "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", - "pincodeRequirementsChars": "PIN må kun inneholde tall", - "passwordRequirementsLength": "Passord må være minst 1 tegn langt", - "passwordRequirementsTitle": "Passordkrav:", - "passwordRequirementLength": "Minst 8 tegn lang", - "passwordRequirementUppercase": "Minst én stor bokstav", - "passwordRequirementLowercase": "Minst én liten bokstav", - "passwordRequirementNumber": "Minst ét tall", - "passwordRequirementSpecial": "Minst ett spesialtegn", - "passwordRequirementsMet": "✓ Passord oppfyller alle krav", - "passwordStrength": "Passordstyrke", - "passwordStrengthWeak": "Svakt", - "passwordStrengthMedium": "Medium", - "passwordStrengthStrong": "Sterkt", - "passwordRequirements": "Krav:", - "passwordRequirementLengthText": "8+ tegn", - "passwordRequirementUppercaseText": "Stor bokstav (A-Z)", - "passwordRequirementLowercaseText": "Liten bokstav (a-z)", - "passwordRequirementNumberText": "Tall (0-9)", - "passwordRequirementSpecialText": "Spesialtegn (!@#$%...)", - "passwordsDoNotMatch": "Passordene stemmer ikke", - "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", - "otpEmailSent": "OTP sendt", - "otpEmailSentDescription": "En OTP er sendt til din e-post", - "otpEmailErrorAuthenticate": "Mislyktes å autentisere med e-post", - "pincodeErrorAuthenticate": "Kunne ikke autentisere med pinkode", - "passwordErrorAuthenticate": "Kunne ikke autentisere med passord", - "poweredBy": "Drevet av", - "authenticationRequired": "Autentisering påkrevd", - "authenticationMethodChoose": "Velg din foretrukne metode for å få tilgang til {name}", - "authenticationRequest": "Du må autentisere deg for å få tilgang til {name}", - "user": "Bruker", - "pincodeInput": "6-sifret PIN-kode", - "pincodeSubmit": "Logg inn med PIN", - "passwordSubmit": "Logg inn med passord", - "otpEmailDescription": "En engangskode vil bli sendt til denne e-posten.", - "otpEmailSend": "Send engangskode", - "otpEmail": "Engangspassord (OTP)", - "otpEmailSubmit": "Send inn OTP", - "backToEmail": "Tilbake til E-post", - "noSupportKey": "Serveren kjører uten en supporterlisens. Vurder å støtte prosjektet!", - "accessDenied": "Tilgang nektet", - "accessDeniedDescription": "Du har ikke tilgang til denne ressursen. Hvis dette er en feil, vennligst kontakt administratoren.", - "accessTokenError": "Feil ved sjekk av tilgangstoken", - "accessGranted": "Tilgang gitt", - "accessUrlInvalid": "Ugyldig tilgangs-URL", - "accessGrantedDescription": "Du har fått tilgang til denne ressursen. Omdirigerer deg...", - "accessUrlInvalidDescription": "Denne delings-URL-en er ugyldig. Vennligst kontakt ressurseieren for en ny URL.", - "tokenInvalid": "Ugyldig token", - "pincodeInvalid": "Ugyldig kode", - "passwordErrorRequestReset": "Forespørsel om tilbakestilling mislyktes", - "passwordErrorReset": "Klarte ikke å tilbakestille passord:", - "passwordResetSuccess": "Passordet er tilbakestilt! Går tilbake til innlogging...", - "passwordReset": "Tilbakestill passord", - "passwordResetDescription": "Følg stegene for å tilbakestille passordet ditt", - "passwordResetSent": "Vi sender en kode for tilbakestilling av passord til denne e-postadressen.", - "passwordResetCode": "Tilbakestillingskode", - "passwordResetCodeDescription": "Sjekk e-posten din for tilbakestillingskoden.", - "passwordNew": "Nytt passord", - "passwordNewConfirm": "Bekreft nytt passord", - "pincodeAuth": "Autentiseringskode", - "pincodeSubmit2": "Send inn kode", - "passwordResetSubmit": "Be om tilbakestilling", - "passwordBack": "Tilbake til passord", - "loginBack": "Gå tilbake til innlogging", - "signup": "Registrer deg", - "loginStart": "Logg inn for å komme i gang", - "idpOidcTokenValidating": "Validerer OIDC-token", - "idpOidcTokenResponse": "Valider OIDC-tokensvar", - "idpErrorOidcTokenValidating": "Feil ved validering av OIDC-token", - "idpConnectingTo": "Kobler til {name}", - "idpConnectingToDescription": "Validerer identiteten din", - "idpConnectingToProcess": "Kobler til...", - "idpConnectingToFinished": "Tilkoblet", - "idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.", - "idpErrorNotFound": "IdP ikke funnet", - "inviteInvalid": "Ugyldig invitasjon", - "inviteInvalidDescription": "Invitasjonslenken er ugyldig.", - "inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren", - "inviteErrorUserNotExists": "Brukeren eksisterer ikke. Vennligst opprett en konto først.", - "inviteErrorLoginRequired": "Du må være logget inn for å godta en invitasjon", - "inviteErrorExpired": "Invitasjonen kan ha utløpt", - "inviteErrorRevoked": "Invitasjonen kan ha blitt trukket tilbake", - "inviteErrorTypo": "Det kan være en skrivefeil i invitasjonslenken", - "pangolinSetup": "Oppsett - Pangolin", - "orgNameRequired": "Organisasjonsnavn er påkrevd", - "orgIdRequired": "Organisasjons-ID er påkrevd", - "orgErrorCreate": "En feil oppstod under oppretting av organisasjon", - "pageNotFound": "Siden ble ikke funnet", - "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", - "overview": "Oversikt", - "home": "Hjem", - "accessControl": "Tilgangskontroll", - "settings": "Innstillinger", - "usersAll": "Alle brukere", - "license": "Lisens", - "pangolinDashboard": "Dashbord - Pangolin", - "noResults": "Ingen resultater funnet.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Inntastede tagger", - "tagsEnteredDescription": "Dette er taggene du har tastet inn.", - "tagsWarnCannotBeLessThanZero": "maxTags og minTags kan ikke være mindre enn 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tagg ikke tillatt i henhold til autofullfør-alternativer", - "tagsWarnInvalid": "Ugyldig tagg i henhold til validateTag", - "tagWarnTooShort": "Tagg {tagText} er for kort", - "tagWarnTooLong": "Tagg {tagText} er for lang", - "tagsWarnReachedMaxNumber": "Maksimalt antall tillatte tagger er nådd", - "tagWarnDuplicate": "Duplisert tagg {tagText} ble ikke lagt til", - "supportKeyInvalid": "Ugyldig nøkkel", - "supportKeyInvalidDescription": "Din supporternøkkel er ugyldig.", - "supportKeyValid": "Gyldig nøkkel", - "supportKeyValidDescription": "Din supporternøkkel er validert. Takk for din støtte!", - "supportKeyErrorValidationDescription": "Klarte ikke å validere supporternøkkel.", - "supportKey": "Støtt utviklingen og adopter en Pangolin!", - "supportKeyDescription": "Kjøp en supporternøkkel for å hjelpe oss med å fortsette utviklingen av Pangolin for fellesskapet. Ditt bidrag lar oss bruke mer tid på å vedlikeholde og legge til nye funksjoner i applikasjonen for alle. Vi vil aldri bruke dette til å legge funksjoner bak en betalingsmur. Dette er atskilt fra enhver kommersiell utgave.", - "supportKeyPet": "Du vil også få adoptere og møte din helt egen kjæledyr-Pangolin!", - "supportKeyPurchase": "Betalinger behandles via GitHub. Etterpå kan du hente nøkkelen din på", - "supportKeyPurchaseLink": "vår nettside", - "supportKeyPurchase2": "og løse den inn her.", - "supportKeyLearnMore": "Lær mer.", - "supportKeyOptions": "Vennligst velg det alternativet som passer deg best.", - "supportKetOptionFull": "Full støttespiller", - "forWholeServer": "For hele serveren", - "lifetimePurchase": "Livstidskjøp", - "supporterStatus": "Supporterstatus", - "buy": "Kjøp", - "supportKeyOptionLimited": "Begrenset støttespiller", - "forFiveUsers": "For 5 eller færre brukere", - "supportKeyRedeem": "Løs inn supporternøkkel", - "supportKeyHideSevenDays": "Skjul i 7 dager", - "supportKeyEnter": "Skriv inn supporternøkkel", - "supportKeyEnterDescription": "Møt din helt egen kjæledyr-Pangolin!", - "githubUsername": "GitHub-brukernavn", - "supportKeyInput": "Supporternøkkel", - "supportKeyBuy": "Kjøp supporternøkkel", - "logoutError": "Feil ved utlogging", - "signingAs": "Logget inn som", - "serverAdmin": "Serveradministrator", - "managedSelfhosted": "Administrert selv-hostet", - "otpEnable": "Aktiver tofaktor", - "otpDisable": "Deaktiver tofaktor", - "logout": "Logg ut", - "licenseTierProfessionalRequired": "Profesjonell utgave påkrevd", - "licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.", - "actionGetOrg": "Hent organisasjon", - "updateOrgUser": "Oppdater org.bruker", - "createOrgUser": "Opprett Org bruker", - "actionUpdateOrg": "Oppdater organisasjon", - "actionUpdateUser": "Oppdater bruker", - "actionGetUser": "Hent bruker", - "actionGetOrgUser": "Hent organisasjonsbruker", - "actionListOrgDomains": "List opp organisasjonsdomener", - "actionCreateSite": "Opprett område", - "actionDeleteSite": "Slett område", - "actionGetSite": "Hent område", - "actionListSites": "List opp områder", - "actionApplyBlueprint": "Bruk blåkopi", - "setupToken": "Oppsetttoken", - "setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.", - "setupTokenRequired": "Oppsetttoken er nødvendig", - "actionUpdateSite": "Oppdater område", - "actionListSiteRoles": "List opp tillatte områderoller", - "actionCreateResource": "Opprett ressurs", - "actionDeleteResource": "Slett ressurs", - "actionGetResource": "Hent ressurs", - "actionListResource": "List opp ressurser", - "actionUpdateResource": "Oppdater ressurs", - "actionListResourceUsers": "List opp ressursbrukere", - "actionSetResourceUsers": "Angi ressursbrukere", - "actionSetAllowedResourceRoles": "Angi tillatte ressursroller", - "actionListAllowedResourceRoles": "List opp tillatte ressursroller", - "actionSetResourcePassword": "Angi ressurspassord", - "actionSetResourcePincode": "Angi ressurspinkode", - "actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs", - "actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs", - "actionCreateTarget": "Opprett mål", - "actionDeleteTarget": "Slett mål", - "actionGetTarget": "Hent mål", - "actionListTargets": "List opp mål", - "actionUpdateTarget": "Oppdater mål", - "actionCreateRole": "Opprett rolle", - "actionDeleteRole": "Slett rolle", - "actionGetRole": "Hent rolle", - "actionListRole": "List opp roller", - "actionUpdateRole": "Oppdater rolle", - "actionListAllowedRoleResources": "List opp tillatte rolleressurser", - "actionInviteUser": "Inviter bruker", - "actionRemoveUser": "Fjern bruker", - "actionListUsers": "List opp brukere", - "actionAddUserRole": "Legg til brukerrolle", - "actionGenerateAccessToken": "Generer tilgangstoken", - "actionDeleteAccessToken": "Slett tilgangstoken", - "actionListAccessTokens": "List opp tilgangstokener", - "actionCreateResourceRule": "Opprett ressursregel", - "actionDeleteResourceRule": "Slett ressursregel", - "actionListResourceRules": "List opp ressursregler", - "actionUpdateResourceRule": "Oppdater ressursregel", - "actionListOrgs": "List opp organisasjoner", - "actionCheckOrgId": "Sjekk ID", - "actionCreateOrg": "Opprett organisasjon", - "actionDeleteOrg": "Slett organisasjon", - "actionListApiKeys": "List opp API-nøkler", - "actionListApiKeyActions": "List opp API-nøkkelhandlinger", - "actionSetApiKeyActions": "Angi tillatte handlinger for API-nøkkel", - "actionCreateApiKey": "Opprett API-nøkkel", - "actionDeleteApiKey": "Slett API-nøkkel", - "actionCreateIdp": "Opprett IDP", - "actionUpdateIdp": "Oppdater IDP", - "actionDeleteIdp": "Slett IDP", - "actionListIdps": "List opp IDP-er", - "actionGetIdp": "Hent IDP", - "actionCreateIdpOrg": "Opprett IDP-organisasjonspolicy", - "actionDeleteIdpOrg": "Slett IDP-organisasjonspolicy", - "actionListIdpOrgs": "List opp IDP-organisasjoner", - "actionUpdateIdpOrg": "Oppdater IDP-organisasjon", - "actionCreateClient": "Opprett Klient", - "actionDeleteClient": "Slett klient", - "actionUpdateClient": "Oppdater klient", - "actionListClients": "List klienter", - "actionGetClient": "Hent klient", - "actionCreateSiteResource": "Opprett stedsressurs", - "actionDeleteSiteResource": "Slett Stedsressurs", - "actionGetSiteResource": "Hent Stedsressurs", - "actionListSiteResources": "List opp Stedsressurser", - "actionUpdateSiteResource": "Oppdater Stedsressurs", - "actionListInvitations": "Liste invitasjoner", - "noneSelected": "Ingen valgt", - "orgNotFound2": "Ingen organisasjoner funnet.", - "searchProgress": "Søker...", - "create": "Opprett", - "orgs": "Organisasjoner", - "loginError": "En feil oppstod under innlogging", - "passwordForgot": "Glemt passordet ditt?", - "otpAuth": "Tofaktorautentisering", - "otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.", - "otpAuthSubmit": "Send inn kode", - "idpContinue": "Eller fortsett med", - "otpAuthBack": "Tilbake til innlogging", - "navbar": "Navigasjonsmeny", - "navbarDescription": "Hovednavigasjonsmeny for applikasjonen", - "navbarDocsLink": "Dokumentasjon", - "otpErrorEnable": "Kunne ikke aktivere 2FA", - "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", - "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", - "otpSetupCheckCodeRetry": "Ugyldig kode. Vennligst prøv igjen.", - "otpSetup": "Aktiver tofaktorautentisering", - "otpSetupDescription": "Sikre kontoen din med et ekstra lag med beskyttelse", - "otpSetupScanQr": "Skann denne QR-koden med autentiseringsappen din eller skriv inn den hemmelige nøkkelen manuelt:", - "otpSetupSecretCode": "Autentiseringskode", - "otpSetupSuccess": "Tofaktorautentisering aktivert", - "otpSetupSuccessStoreBackupCodes": "Kontoen din er nå sikrere. Ikke glem å lagre reservekodene dine.", - "otpErrorDisable": "Kunne ikke deaktivere 2FA", - "otpErrorDisableDescription": "En feil oppstod under deaktivering av 2FA", - "otpRemove": "Deaktiver tofaktorautentisering", - "otpRemoveDescription": "Deaktiver tofaktorautentisering for kontoen din", - "otpRemoveSuccess": "Tofaktorautentisering deaktivert", - "otpRemoveSuccessMessage": "Tofaktorautentisering er deaktivert for kontoen din. Du kan aktivere den igjen når som helst.", - "otpRemoveSubmit": "Deaktiver 2FA", - "paginator": "Side {current} av {last}", - "paginatorToFirst": "Gå til første side", - "paginatorToPrevious": "Gå til forrige side", - "paginatorToNext": "Gå til neste side", - "paginatorToLast": "Gå til siste side", - "copyText": "Kopier tekst", - "copyTextFailed": "Klarte ikke å kopiere tekst: ", - "copyTextClipboard": "Kopier til utklippstavle", - "inviteErrorInvalidConfirmation": "Ugyldig bekreftelse", - "passwordRequired": "Passord er påkrevd", - "allowAll": "Tillat alle", - "permissionsAllowAll": "Tillat alle rettigheter", - "githubUsernameRequired": "GitHub-brukernavn er påkrevd", - "supportKeyRequired": "supporternøkkel er påkrevd", - "passwordRequirementsChars": "Passordet må være minst 8 tegn", - "language": "Språk", - "verificationCodeRequired": "Kode er påkrevd", - "userErrorNoUpdate": "Ingen bruker å oppdatere", - "siteErrorNoUpdate": "Ingen område å oppdatere", - "resourceErrorNoUpdate": "Ingen ressurs å oppdatere", - "authErrorNoUpdate": "Ingen autentiseringsinfo å oppdatere", - "orgErrorNoUpdate": "Ingen organisasjon å oppdatere", - "orgErrorNoProvided": "Ingen organisasjon angitt", - "apiKeysErrorNoUpdate": "Ingen API-nøkkel å oppdatere", - "sidebarOverview": "Oversikt", - "sidebarHome": "Hjem", - "sidebarSites": "Områder", - "sidebarResources": "Ressurser", - "sidebarAccessControl": "Tilgangskontroll", - "sidebarUsers": "Brukere", - "sidebarInvitations": "Invitasjoner", - "sidebarRoles": "Roller", - "sidebarShareableLinks": "Delbare lenker", - "sidebarApiKeys": "API-nøkler", - "sidebarSettings": "Innstillinger", - "sidebarAllUsers": "Alle brukere", - "sidebarIdentityProviders": "Identitetsleverandører", - "sidebarLicense": "Lisens", - "sidebarClients": "Clients", - "sidebarDomains": "Domener", - "enableDockerSocket": "Aktiver Docker blåkopi", - "enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.", - "enableDockerSocketLink": "Lær mer", - "viewDockerContainers": "Vis Docker-containere", - "containersIn": "Containere i {siteName}", - "selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.", - "containerName": "Navn", - "containerImage": "Bilde", - "containerState": "Tilstand", - "containerNetworks": "Nettverk", - "containerHostnameIp": "Vertsnavn/IP", - "containerLabels": "Etiketter", - "containerLabelsCount": "{count, plural, one {en etikett} other {# etiketter}}", - "containerLabelsTitle": "Containeretiketter", - "containerLabelEmpty": "", - "containerPorts": "Porter", - "containerPortsMore": "+{count} til", - "containerActions": "Handlinger", - "select": "Velg", - "noContainersMatchingFilters": "Ingen containere funnet som matcher de nåværende filtrene.", - "showContainersWithoutPorts": "Vis containere uten porter", - "showStoppedContainers": "Vis stoppede containere", - "noContainersFound": "Ingen containere funnet. Sørg for at Docker-containere kjører.", - "searchContainersPlaceholder": "Søk blant {count} containere...", - "searchResultsCount": "{count, plural, one {ett resultat} other {# resultater}}", - "filters": "Filtre", - "filterOptions": "Filteralternativer", - "filterPorts": "Porter", - "filterStopped": "Stoppet", - "clearAllFilters": "Fjern alle filtre", - "columns": "Kolonner", - "toggleColumns": "Vis/skjul kolonner", - "refreshContainersList": "Oppdater containerliste", - "searching": "Søker...", - "noContainersFoundMatching": "Ingen containere funnet som matcher \"{filter}\".", - "light": "lys", - "dark": "mørk", - "system": "system", - "theme": "Tema", - "subnetRequired": "Subnett er påkrevd", - "initialSetupTitle": "Førstegangsoppsett av server", - "initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.", - "createAdminAccount": "Opprett administratorkonto", - "setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.", - "certificateStatus": "Sertifikatstatus", - "loading": "Laster inn", - "restart": "Start på nytt", - "domains": "Domener", - "domainsDescription": "Administrer domener for organisasjonen din", - "domainsSearch": "Søk i domener...", - "domainAdd": "Legg til domene", - "domainAddDescription": "Registrer et nytt domene hos organisasjonen din", - "domainCreate": "Opprett domene", - "domainCreatedDescription": "Domene ble opprettet", - "domainDeletedDescription": "Domene ble slettet", - "domainQuestionRemove": "Er du sikker på at du vil fjerne domenet {domain} fra kontoen din?", - "domainMessageRemove": "Når domenet er fjernet, vil det ikke lenger være knyttet til kontoen din.", - "domainMessageConfirm": "For å bekrefte, vennligst skriv inn domenenavnet nedenfor.", - "domainConfirmDelete": "Bekreft sletting av domene", - "domainDelete": "Slett domene", - "domain": "Domene", - "selectDomainTypeNsName": "Domenedelegering (NS)", - "selectDomainTypeNsDescription": "Dette domenet og alle dets underdomener. Bruk dette når du vil kontrollere en hel domenesone.", - "selectDomainTypeCnameName": "Enkelt domene (CNAME)", - "selectDomainTypeCnameDescription": "Bare dette spesifikke domenet. Bruk dette for individuelle underdomener eller spesifikke domeneoppføringer.", - "selectDomainTypeWildcardName": "Wildcard-domene", - "selectDomainTypeWildcardDescription": "Dette domenet og dets underdomener.", - "domainDelegation": "Enkelt domene", - "selectType": "Velg en type", - "actions": "Handlinger", - "refresh": "Oppdater", - "refreshError": "Klarte ikke å oppdatere data", - "verified": "Verifisert", - "pending": "Venter", - "sidebarBilling": "Fakturering", - "billing": "Fakturering", - "orgBillingDescription": "Administrer faktureringsinformasjon og abonnementer", - "github": "GitHub", - "pangolinHosted": "Driftet av Pangolin", - "fossorial": "Fossorial", - "completeAccountSetup": "Fullfør kontooppsett", - "completeAccountSetupDescription": "Angi passordet ditt for å komme i gang", - "accountSetupSent": "Vi sender en oppsettskode for kontoen til denne e-postadressen.", - "accountSetupCode": "Oppsettskode", - "accountSetupCodeDescription": "Sjekk e-posten din for oppsettskoden.", - "passwordCreate": "Opprett passord", - "passwordCreateConfirm": "Bekreft passord", - "accountSetupSubmit": "Send oppsettskode", - "completeSetup": "Fullfør oppsett", - "accountSetupSuccess": "Kontooppsett fullført! Velkommen til Pangolin!", - "documentation": "Dokumentasjon", - "saveAllSettings": "Lagre alle innstillinger", - "settingsUpdated": "Innstillinger oppdatert", - "settingsUpdatedDescription": "Alle innstillinger er oppdatert", - "settingsErrorUpdate": "Klarte ikke å oppdatere innstillinger", - "settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger", - "sidebarCollapse": "Skjul", - "sidebarExpand": "Utvid", - "newtUpdateAvailable": "Oppdatering tilgjengelig", - "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", - "domainPickerEnterDomain": "Domene", - "domainPickerPlaceholder": "minapp.eksempel.no", - "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", - "domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer", - "domainPickerTabAll": "Alle", - "domainPickerTabOrganization": "Organisasjon", - "domainPickerTabProvided": "Levert", - "domainPickerSortAsc": "A-Å", - "domainPickerSortDesc": "Å-A", - "domainPickerCheckingAvailability": "Sjekker tilgjengelighet...", - "domainPickerNoMatchingDomains": "Ingen samsvarende domener funnet. Prøv et annet domene eller sjekk organisasjonens domeneinnstillinger.", - "domainPickerOrganizationDomains": "Organisasjonsdomener", - "domainPickerProvidedDomains": "Leverte domener", - "domainPickerSubdomain": "Underdomene: {subdomain}", - "domainPickerNamespace": "Navnerom: {namespace}", - "domainPickerShowMore": "Vis mer", - "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", - "createNewOrgDescription": "Opprett en ny organisasjon", - "organization": "Organisasjon", - "port": "Port", - "securityKeyManage": "Administrer sikkerhetsnøkler", - "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", - "securityKeyRegister": "Registrer ny sikkerhetsnøkkel", - "securityKeyList": "Dine sikkerhetsnøkler", - "securityKeyNone": "Ingen sikkerhetsnøkler er registrert enda", - "securityKeyNameRequired": "Navn er påkrevd", - "securityKeyRemove": "Fjern", - "securityKeyLastUsed": "Sist brukt: {date}", - "securityKeyNameLabel": "Navn på sikkerhetsnøkkel", - "securityKeyRegisterSuccess": "Sikkerhetsnøkkel registrert", - "securityKeyRegisterError": "Klarte ikke å registrere sikkerhetsnøkkel", - "securityKeyRemoveSuccess": "Sikkerhetsnøkkel fjernet", - "securityKeyRemoveError": "Klarte ikke å fjerne sikkerhetsnøkkel", - "securityKeyLoadError": "Klarte ikke å laste inn sikkerhetsnøkler", - "securityKeyLogin": "Fortsett med sikkerhetsnøkkel", - "securityKeyAuthError": "Klarte ikke å autentisere med sikkerhetsnøkkel", - "securityKeyRecommendation": "Registrer en reservesikkerhetsnøkkel på en annen enhet for å sikre at du alltid har tilgang til kontoen din.", - "registering": "Registrerer...", - "securityKeyPrompt": "Vennligst verifiser identiteten din med sikkerhetsnøkkelen. Sørg for at sikkerhetsnøkkelen er koblet til og klar.", - "securityKeyBrowserNotSupported": "Nettleseren din støtter ikke sikkerhetsnøkler. Vennligst bruk en moderne nettleser som Chrome, Firefox eller Safari.", - "securityKeyPermissionDenied": "Vennligst tillat tilgang til sikkerhetsnøkkelen din for å fortsette innloggingen.", - "securityKeyRemovedTooQuickly": "Vennligst hold sikkerhetsnøkkelen tilkoblet til innloggingsprosessen er fullført.", - "securityKeyNotSupported": "Sikkerhetsnøkkelen din er kanskje ikke kompatibel. Vennligst prøv en annen sikkerhetsnøkkel.", - "securityKeyUnknownError": "Det oppstod et problem med å bruke sikkerhetsnøkkelen din. Vennligst prøv igjen.", - "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", - "twoFactor": "Tofaktorautentisering", - "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", - "securityKeyAdd": "Legg til sikkerhetsnøkkel", - "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", - "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", - "securityKeyTwoFactorRequired": "Tofaktorautentisering påkrevd", - "securityKeyTwoFactorDescription": "Vennligst skriv inn koden for tofaktorautentisering for å registrere sikkerhetsnøkkelen", - "securityKeyTwoFactorRemoveDescription": "Vennligst skriv inn koden for tofaktorautentisering for å fjerne sikkerhetsnøkkelen", - "securityKeyTwoFactorCode": "Tofaktorkode", - "securityKeyRemoveTitle": "Fjern sikkerhetsnøkkel", - "securityKeyRemoveDescription": "Skriv inn passordet ditt for å fjerne sikkerhetsnøkkelen \"{name}\"", - "securityKeyNoKeysRegistered": "Ingen sikkerhetsnøkler registrert", - "securityKeyNoKeysDescription": "Legg til en sikkerhetsnøkkel for å øke sikkerheten på kontoen din", - "createDomainRequired": "Domene er påkrevd", - "createDomainAddDnsRecords": "Legg til DNS-oppføringer", - "createDomainAddDnsRecordsDescription": "Legg til følgende DNS-oppføringer hos din domeneleverandør for å fullføre oppsettet.", - "createDomainNsRecords": "NS-oppføringer", - "createDomainRecord": "Oppføring", - "createDomainType": "Type:", - "createDomainName": "Navn:", - "createDomainValue": "Verdi:", - "createDomainCnameRecords": "CNAME-oppføringer", - "createDomainARecords": "A-oppføringer", - "createDomainRecordNumber": "Oppføring {number}", - "createDomainTxtRecords": "TXT-oppføringer", - "createDomainSaveTheseRecords": "Lagre disse oppføringene", - "createDomainSaveTheseRecordsDescription": "Sørg for å lagre disse DNS-oppføringene, da du ikke vil se dem igjen.", - "createDomainDnsPropagation": "DNS-propagering", - "createDomainDnsPropagationDescription": "DNS-endringer kan ta litt tid å propagere over internett. Dette kan ta fra noen få minutter til 48 timer, avhengig av din DNS-leverandør og TTL-innstillinger.", - "resourcePortRequired": "Portnummer er påkrevd for ikke-HTTP-ressurser", - "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressurser", - "billingPricingCalculatorLink": "Pris Kalkulator", - "signUpTerms": { - "IAgreeToThe": "Jeg godtar", - "termsOfService": "brukervilkårene", - "and": "og", - "privacyPolicy": "personvernerklæringen" - }, - "siteRequired": "Område er påkrevd.", - "olmTunnel": "Olm-tunnel", - "olmTunnelDescription": "Bruk Olm for klienttilkobling", - "errorCreatingClient": "Feil ved oppretting av klient", - "clientDefaultsNotFound": "Klientstandarder ikke funnet", - "createClient": "Opprett klient", - "createClientDescription": "Opprett en ny klient for å koble til dine områder", - "seeAllClients": "Se alle klienter", - "clientInformation": "Klientinformasjon", - "clientNamePlaceholder": "Klientnavn", - "address": "Adresse", - "subnetPlaceholder": "Subnett", - "addressDescription": "Adressen denne klienten vil bruke for tilkobling", - "selectSites": "Velg områder", - "sitesDescription": "Klienten vil ha tilkobling til de valgte områdene", - "clientInstallOlm": "Installer Olm", - "clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt", - "clientOlmCredentials": "Olm-legitimasjon", - "clientOlmCredentialsDescription": "Slik vil Olm autentisere med serveren", - "olmEndpoint": "Olm-endepunkt", - "olmId": "Olm-ID", - "olmSecretKey": "Olm hemmelig nøkkel", - "clientCredentialsSave": "Lagre din legitimasjon", - "clientCredentialsSaveDescription": "Du vil bare kunne se dette én gang. Sørg for å kopiere det til et sikkert sted.", - "generalSettingsDescription": "Konfigurer de generelle innstillingene for denne klienten", - "clientUpdated": "Klient oppdatert", - "clientUpdatedDescription": "Klienten er blitt oppdatert.", - "clientUpdateFailed": "Klarte ikke å oppdatere klient", - "clientUpdateError": "En feil oppstod under oppdatering av klienten.", - "sitesFetchFailed": "Klarte ikke å hente områder", - "sitesFetchError": "En feil oppstod under henting av områder.", - "olmErrorFetchReleases": "En feil oppstod under henting av Olm-utgivelser.", - "olmErrorFetchLatest": "En feil oppstod under henting av den nyeste Olm-utgivelsen.", - "remoteSubnets": "Fjern-subnett", - "enterCidrRange": "Skriv inn CIDR-område", - "remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.", - "resourceEnableProxy": "Aktiver offentlig proxy", - "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", - "externalProxyEnabled": "Ekstern proxy aktivert", - "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", - "domainPickerBaseDomainLabel": "Grunndomene", - "domainPickerSearchDomains": "Søk i domener...", - "domainPickerNoDomainsFound": "Ingen domener funnet", - "domainPickerLoadingDomains": "Laster inn domener...", - "domainPickerSelectBaseDomain": "Velg grunndomene...", - "domainPickerNotAvailableForCname": "Ikke tilgjengelig for CNAME-domener", - "domainPickerEnterSubdomainOrLeaveBlank": "Skriv inn underdomene eller la feltet stå tomt for å bruke grunndomene.", - "domainPickerEnterSubdomainToSearch": "Skriv inn et underdomene for å søke og velge blant tilgjengelige gratis domener.", - "domainPickerFreeDomains": "Gratis domener", - "domainPickerSearchForAvailableDomains": "Søk etter tilgjengelige domener", - "domainPickerNotWorkSelfHosted": "Merk: Gratis tilbudte domener er ikke tilgjengelig for selv-hostede instanser akkurat nå.", - "resourceDomain": "Domene", - "resourceEditDomain": "Rediger domene", - "siteName": "Områdenavn", - "proxyPort": "Port", - "resourcesTableProxyResources": "Proxy-ressurser", - "resourcesTableClientResources": "Klientressurser", - "resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.", - "resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.", - "resourcesTableDestination": "Destinasjon", - "resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med", - "resourcesTableClients": "Klienter", - "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.", - "editInternalResourceDialogEditClientResource": "Rediger klientressurs", - "editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.", - "editInternalResourceDialogResourceProperties": "Ressursegenskaper", - "editInternalResourceDialogName": "Navn", - "editInternalResourceDialogProtocol": "Protokoll", - "editInternalResourceDialogSitePort": "Områdeport", - "editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", - "editInternalResourceDialogCancel": "Avbryt", - "editInternalResourceDialogSaveResource": "Lagre ressurs", - "editInternalResourceDialogSuccess": "Suksess", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressurs oppdatert vellykket", - "editInternalResourceDialogError": "Feil", - "editInternalResourceDialogFailedToUpdateInternalResource": "Mislyktes å oppdatere intern ressurs", - "editInternalResourceDialogNameRequired": "Navn er påkrevd", - "editInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn", - "editInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1", - "editInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", - "editInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", - "editInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", - "createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder", - "createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et konfigureret delnett for å lage interne ressurser.", - "createInternalResourceDialogClose": "Lukk", - "createInternalResourceDialogCreateClientResource": "Opprett klientressurs", - "createInternalResourceDialogCreateClientResourceDescription": "Lag en ny ressurs som blir tilgjengelig for klienter koblet til det valgte området.", - "createInternalResourceDialogResourceProperties": "Ressursegenskaper", - "createInternalResourceDialogName": "Navn", - "createInternalResourceDialogSite": "Område", - "createInternalResourceDialogSelectSite": "Velg område...", - "createInternalResourceDialogSearchSites": "Søk i områder...", - "createInternalResourceDialogNoSitesFound": "Ingen områder funnet.", - "createInternalResourceDialogProtocol": "Protokoll", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Områdeport", - "createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.", - "createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon", - "createInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.", - "createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.", - "createInternalResourceDialogCancel": "Avbryt", - "createInternalResourceDialogCreateResource": "Opprett ressurs", - "createInternalResourceDialogSuccess": "Suksess", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressurs opprettet vellykket", - "createInternalResourceDialogError": "Feil", - "createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette intern ressurs", - "createInternalResourceDialogNameRequired": "Navn er påkrevd", - "createInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn", - "createInternalResourceDialogPleaseSelectSite": "Vennligst velg et område", - "createInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1", - "createInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat", - "createInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1", - "createInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536", - "siteConfiguration": "Konfigurasjon", - "siteAcceptClientConnections": "Godta klientforbindelser", - "siteAcceptClientConnectionsDescription": "Tillat andre enheter å koble seg til gjennom denne Newt-instansen som en gateway ved hjelp av klienter.", - "siteAddress": "Områdeadresse", - "siteAddressDescription": "Angi IP-adressen til verten for klienter å koble seg til. Dette er den interne adressen til området i Pangolin-nettverket for klienter som adresserer. Må falle innenfor Org-underettet.", - "autoLoginExternalIdp": "Automatisk innlogging med ekstern IDP", - "autoLoginExternalIdpDescription": "Omdiriger brukeren umiddelbart til den eksterne IDP-en for autentisering.", - "selectIdp": "Velg IDP", - "selectIdpPlaceholder": "Velg en IDP...", - "selectIdpRequired": "Vennligst velg en IDP når automatisk innlogging er aktivert.", - "autoLoginTitle": "Omdirigering", - "autoLoginDescription": "Omdirigerer deg til den eksterne identitetsleverandøren for autentisering.", - "autoLoginProcessing": "Forbereder autentisering...", - "autoLoginRedirecting": "Omdirigerer til innlogging...", - "autoLoginError": "Feil ved automatisk innlogging", - "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.", - "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.", - "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", - "introTitle": "Administrert Self-Hosted Pangolin", - "introDescription": "er et alternativ for bruk utviklet for personer som ønsker enkel og ekstra pålitelighet mens de fortsatt holder sine data privat og selvdrevne.", - "introDetail": "Med dette valget kjører du fortsatt din egen Pangolin-node - tunneler, SSL-terminering og trafikken ligger på serveren din. Forskjellen er at behandling og overvåking håndteres gjennom vårt skydashbord, som låser opp en rekke fordeler:", - "benefitSimplerOperations": { - "title": "Enklere operasjoner", - "description": "Ingen grunn til å kjøre din egen e-postserver eller sette opp kompleks varsling. Du vil få helsesjekk og nedetid varsler ut av boksen." - }, - "benefitAutomaticUpdates": { - "title": "Automatiske oppdateringer", - "description": "Cloud dashbordet utvikler seg raskt, så du får nye funksjoner og feilrettinger uten at du trenger å trekke nye beholdere manuelt hver gang." - }, - "benefitLessMaintenance": { - "title": "Mindre vedlikehold", - "description": "Ingen databasestyrer, sikkerhetskopier eller ekstra infrastruktur for å forvalte. Vi håndterer det i skyen." - }, - "benefitCloudFailover": { - "title": "Sky feilslått", - "description": "Hvis EK-gruppen din går ned, kan tunnlene midlertidig mislykkes i å nå våre sky-punkter til du tar den tilbake på nett." - }, - "benefitHighAvailability": { - "title": "Høy tilgjengelighet (PoPs)", - "description": "Du kan også legge ved flere noder til kontoen din for redundans og bedre ytelse." - }, - "benefitFutureEnhancements": { - "title": "Fremtidige forbedringer", - "description": "Vi planlegger å legge inn mer analyser, varsle og styringsverktøy for å gjøre din distribusjon enda mer robust." - }, - "docsAlert": { - "text": "Lær mer om Managed Self-Hosted alternativet i vår", - "documentation": "dokumentasjon" - }, - "convertButton": "Konverter denne noden til manuelt bruk" - }, - "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", - "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", - "domainPickerUnverified": "Uverifisert", - "domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.", - "domainPickerError": "Feil", - "domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener", - "domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domenetilgjengelighet", - "domainPickerInvalidSubdomain": "Ugyldig underdomene", - "domainPickerInvalidSubdomainRemoved": "Inndata \"{sub}\" ble fjernet fordi det ikke er gyldig.", - "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", - "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 deleted file mode 100644 index 25181569..00000000 --- a/messages/nl-NL.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Maak uw organisatie, site en bronnen aan", - "setupNewOrg": "Nieuwe organisatie", - "setupCreateOrg": "Nieuwe organisatie aanmaken", - "setupCreateResources": "Bronnen aanmaken", - "setupOrgName": "Naam van de organisatie", - "orgDisplayName": "Dit is de weergavenaam van uw organisatie.", - "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!", - "welcomeTo": "Welkom bij", - "componentsCreateOrg": "Maak een Organisatie", - "componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} one {één organisatie} other {# organisaties}}.", - "componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.", - "dismiss": "Uitschakelen", - "componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.", - "componentsSupporterMessage": "Bedankt voor het ondersteunen van Pangolin als {tier}!", - "inviteErrorNotValid": "Het spijt ons, maar de uitnodiging die je probeert te bezoeken is niet geaccepteerd of is niet meer geldig.", - "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.", - "goHome": "Ga naar huis", - "inviteLogInOtherUser": "Log in als een andere gebruiker", - "createAnAccount": "Account aanmaken", - "inviteNotAccepted": "Uitnodiging niet geaccepteerd", - "authCreateAccount": "Maak een account aan om te beginnen", - "authNoAccount": "Nog geen account?", - "email": "E-mailadres", - "password": "Wachtwoord", - "confirmPassword": "Bevestig wachtwoord", - "createAccount": "Account Aanmaken", - "viewSettings": "Instellingen weergeven", - "delete": "Verwijderen", - "name": "Naam", - "online": "Online", - "offline": "Offline", - "site": "Referentie", - "dataIn": "Dataverbruik inkomend", - "dataOut": "Dataverbruik uitgaand", - "connectionType": "Type verbinding", - "tunnelType": "Tunnel type", - "local": "Lokaal", - "edit": "Bewerken", - "siteConfirmDelete": "Verwijderen van site bevestigen", - "siteDelete": "Site verwijderen", - "siteMessageRemove": "Eenmaal verwijderd, zal de site niet langer toegankelijk zijn. Alle bronnen en doelen die aan de site zijn gekoppeld, zullen ook worden verwijderd.", - "siteMessageConfirm": "Typ ter bevestiging de naam van de site hieronder.", - "siteQuestionRemove": "Weet u zeker dat u de site {selectedSite} uit de organisatie wilt verwijderen?", - "siteManageSites": "Sites beheren", - "siteDescription": "Verbindt met uw netwerk via beveiligde tunnels", - "siteCreate": "Site maken", - "siteCreateDescription2": "Volg de onderstaande stappen om een nieuwe site aan te maken en te verbinden", - "siteCreateDescription": "Maak een nieuwe site aan om verbinding te maken met uw bronnen", - "close": "Sluiten", - "siteErrorCreate": "Fout bij maken site", - "siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden", - "siteErrorCreateDefaults": "Standaardinstellingen niet gevonden", - "method": "Methode", - "siteMethodDescription": "Op deze manier legt u verbindingen bloot.", - "siteLearnNewt": "Leer hoe u Newt kunt installeren op uw systeem", - "siteSeeConfigOnce": "U kunt de configuratie maar één keer zien.", - "siteLoadWGConfig": "WireGuard configuratie wordt geladen...", - "siteDocker": "Details Docker implementatie uitvouwen", - "toggle": "Omschakelen", - "dockerCompose": "Docker opstellen", - "dockerRun": "Docker Uitvoeren", - "siteLearnLocal": "Lokale sites doen geen tunnel, leren meer", - "siteConfirmCopy": "Ik heb de configuratie gekopieerd", - "searchSitesProgress": "Sites zoeken...", - "siteAdd": "Site toevoegen", - "siteInstallNewt": "Installeer Newt", - "siteInstallNewtDescription": "Laat Newt draaien op uw systeem", - "WgConfiguration": "WireGuard Configuratie", - "WgConfigurationDescription": "Gebruik de volgende configuratie om verbinding te maken met je netwerk", - "operatingSystem": "Operating systeem", - "commands": "Opdrachten", - "recommended": "Aanbevolen", - "siteNewtDescription": "Gebruik Newt voor de beste gebruikerservaring. Het maakt gebruik van WireGuard onder de capuchon en laat je toe om contact op te nemen met je privébronnen via hun LAN-adres op je privénetwerk vanuit het Pangolin dashboard.", - "siteRunsInDocker": "Loopt in Docker", - "siteRunsInShell": "Voert in shell op macOS, Linux en Windows", - "siteErrorDelete": "Fout bij verwijderen site", - "siteErrorUpdate": "Bijwerken site mislukt", - "siteErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de site.", - "siteUpdated": "Site bijgewerkt", - "siteUpdatedDescription": "De site is bijgewerkt.", - "siteGeneralDescription": "Algemene instellingen voor deze site configureren", - "siteSettingDescription": "Configureer de instellingen op uw site", - "siteSetting": "{siteName} instellingen", - "siteNewtTunnel": "Newttunnel (Aanbevolen)", - "siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.", - "siteWg": "Basis WireGuard", - "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", - "siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES", - "siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only 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", - "siteCredentialsSave": "Uw referenties opslaan", - "siteCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.", - "siteInfo": "Site informatie", - "status": "Status", - "shareTitle": "Beheer deellinks", - "shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot uw bronnen te verlenen", - "shareSearch": "Zoek share links...", - "shareCreate": "Maak Share link", - "shareErrorDelete": "Kan link niet verwijderen", - "shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link", - "shareDeleted": "Link verwijderd", - "shareDeletedDescription": "De link is verwijderd", - "shareTokenDescription": "Uw toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de header van de aanvraag. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.", - "accessToken": "Toegangs-token", - "usageExamples": "Voorbeelden van gebruik", - "tokenId": "Token ID", - "requestHeades": "Aanvraag van headers", - "queryParameter": "Queryparameter", - "importantNote": "Belangrijke opmerking", - "shareImportantDescription": "Om veiligheidsredenen wordt het gebruik van headers aanbevolen over queryparameters indien mogelijk, omdat query parameters kunnen worden aangemeld in serverlogboeken of browsergeschiedenis.", - "token": "Token", - "shareTokenSecurety": "Houd uw toegangstoken veilig. Deel deze niet in openbaar toegankelijke gebieden of client-side code.", - "shareErrorFetchResource": "Fout bij het ophalen van bronnen", - "shareErrorFetchResourceDescription": "Er is een fout opgetreden bij het ophalen van de resources", - "shareErrorCreate": "Aanmaken van link delen mislukt", - "shareErrorCreateDescription": "Fout opgetreden tijdens het maken van de share link", - "shareCreateDescription": "Iedereen met deze link heeft toegang tot de pagina", - "shareTitleOptional": "Titel (optioneel)", - "expireIn": "Vervalt in", - "neverExpire": "Nooit verlopen", - "shareExpireDescription": "Vervaltijd is hoe lang de link bruikbaar is en geeft toegang tot de bron. Na deze tijd zal de link niet meer werken en zullen gebruikers die deze link hebben gebruikt de toegang tot de pagina verliezen.", - "shareSeeOnce": "Je kunt deze koppeling alleen zien. Zorg ervoor dat je het kopieert.", - "shareAccessHint": "Iedereen met deze link heeft toegang tot de bron. Deel deze met zorg.", - "shareTokenUsage": "Zie Toegangstoken Gebruik", - "createLink": "Koppeling aanmaken", - "resourcesNotFound": "Geen bronnen gevonden", - "resourceSearch": "Zoek bronnen", - "openMenu": "Menu openen", - "resource": "Bron", - "title": "Aanspreektitel", - "created": "Aangemaakt", - "expires": "Verloopt", - "never": "Nooit", - "shareErrorSelectResource": "Selecteer een bron", - "resourceTitle": "Bronnen beheren", - "resourceDescription": "Veilige proxy's voor uw privéapplicaties maken", - "resourcesSearch": "Zoek bronnen...", - "resourceAdd": "Bron toevoegen", - "resourceErrorDelte": "Fout bij verwijderen document", - "authentication": "Authenticatie", - "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.", - "resourceQuestionRemove": "Weet u zeker dat u de resource {selectedResource} uit de organisatie wilt verwijderen?", - "resourceHTTP": "HTTPS bron", - "resourceHTTPDescription": "Proxy verzoeken aan uw app via HTTPS via een subdomein of basisdomein.", - "resourceRaw": "TCP/UDP bron", - "resourceRawDescription": "Proxy verzoeken naar je app via TCP/UDP met behulp van een poortnummer.", - "resourceCreate": "Bron maken", - "resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken", - "resourceSeeAll": "Alle bronnen bekijken", - "resourceInfo": "Bron informatie", - "resourceNameDescription": "Dit is de weergavenaam voor het document.", - "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", - "resourceHTTPSSettings": "HTTPS instellingen", - "resourceHTTPSSettingsDescription": "Stel in hoe de bron wordt benaderd via HTTPS", - "domainType": "Domein type", - "subdomain": "Subdomein", - "baseDomain": "Basis domein", - "subdomnainDescription": "Het subdomein waar de bron toegankelijk is.", - "resourceRawSettings": "TCP/UDP instellingen", - "resourceRawSettingsDescription": "Stel in hoe je bron wordt benaderd via TCP/UDP", - "protocol": "Protocol", - "protocolSelect": "Selecteer een protocol", - "resourcePortNumber": "Nummer van poort", - "resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.", - "cancel": "Annuleren", - "resourceConfig": "Configuratie tekstbouwstenen", - "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om je TCP/UDP-bron in te stellen", - "resourceAddEntrypoints": "Traefik: Entrypoints toevoegen", - "resourceExposePorts": "Gerbild: Gevangen blootstellen in Docker Compose", - "resourceLearnRaw": "Leer hoe je TCP/UDP bronnen kunt configureren", - "resourceBack": "Terug naar bronnen", - "resourceGoTo": "Ga naar Resource", - "resourceDelete": "Document verwijderen", - "resourceDeleteConfirm": "Bevestig Verwijderen Document", - "visibility": "Zichtbaarheid", - "enabled": "Ingeschakeld", - "disabled": "Uitgeschakeld", - "general": "Algemeen", - "generalSettings": "Algemene instellingen", - "proxy": "Proxy", - "internal": "Intern", - "rules": "Regels", - "resourceSettingDescription": "Configureer de instellingen op uw bron", - "resourceSetting": "{resourceName} instellingen", - "alwaysAllow": "Altijd toestaan", - "alwaysDeny": "Altijd weigeren", - "passToAuth": "Passeren naar Auth", - "orgSettingsDescription": "Configureer de algemene instellingen van je organisatie", - "orgGeneralSettings": "Organisatie Instellingen", - "orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie", - "saveGeneralSettings": "Algemene instellingen opslaan", - "saveSettings": "Instellingen opslaan", - "orgDangerZone": "Gevaarlijke zone", - "orgDangerZoneDescription": "Deze instantie verwijderen is onomkeerbaar. Bevestig alstublieft dat u wilt doorgaan.", - "orgDelete": "Verwijder organisatie", - "orgDeleteConfirm": "Bevestig Verwijderen Organisatie", - "orgMessageRemove": "Deze actie is onomkeerbaar en zal alle bijbehorende gegevens verwijderen.", - "orgMessageConfirm": "Om te bevestigen, typ de naam van de onderstaande organisatie in.", - "orgQuestionRemove": "Weet u zeker dat u de organisatie {selectedOrg} wilt verwijderen?", - "orgUpdated": "Organisatie bijgewerkt", - "orgUpdatedDescription": "De organisatie is bijgewerkt.", - "orgErrorUpdate": "Bijwerken organisatie mislukt", - "orgErrorUpdateMessage": "Fout opgetreden tijdens het bijwerken van de organisatie.", - "orgErrorFetch": "Organisaties ophalen mislukt", - "orgErrorFetchMessage": "Er is een fout opgetreden tijdens het plaatsen van uw organisaties", - "orgErrorDelete": "Kan organisatie niet verwijderen", - "orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.", - "orgDeleted": "Organisatie verwijderd", - "orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.", - "orgMissing": "Organisatie-ID ontbreekt", - "orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.", - "accessUsersManage": "Gebruikers beheren", - "accessUsersDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot uw organisatie te beheren", - "accessUsersSearch": "Gebruikers zoeken...", - "accessUserCreate": "Gebruiker aanmaken", - "accessUserRemove": "Gebruiker verwijderen", - "username": "Gebruikersnaam", - "identityProvider": "Identiteit Provider", - "role": "Functie", - "nameRequired": "Naam is verplicht", - "accessRolesManage": "Rollen beheren", - "accessRolesDescription": "Configureer rollen om toegang tot uw organisatie te beheren", - "accessRolesSearch": "Rollen zoeken...", - "accessRolesAdd": "Rol toevoegen", - "accessRoleDelete": "Verwijder rol", - "description": "Beschrijving", - "inviteTitle": "Open uitnodigingen", - "inviteDescription": "Beheer je uitnodigingen aan andere gebruikers", - "inviteSearch": "Uitnodigingen zoeken...", - "minutes": "minuten", - "hours": "Uren", - "days": "dagen", - "weeks": "Weken", - "months": "maanden", - "years": "Jaar", - "day": "{count, plural, one {# dag} other {# dagen}}", - "apiKeysTitle": "API Key Informatie", - "apiKeysConfirmCopy2": "Bevestig dat u de API-sleutel hebt gekopieerd.", - "apiKeysErrorCreate": "Fout bij maken API-sleutel", - "apiKeysErrorSetPermission": "Fout instellen permissies", - "apiKeysCreate": "API-sleutel genereren", - "apiKeysCreateDescription": "Genereer een nieuwe API-sleutel voor uw organisatie", - "apiKeysGeneralSettings": "Machtigingen", - "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 veilige plek.", - "apiKeysInfo": "Uw API-sleutel is:", - "apiKeysConfirmCopy": "Ik heb de API-sleutel gekopieerd", - "generate": "Genereren", - "done": "Voltooid", - "apiKeysSeeAll": "Alle API-sleutels bekijken", - "apiKeysPermissionsErrorLoadingActions": "Fout bij het laden van API key acties", - "apiKeysPermissionsErrorUpdate": "Fout instellen permissies", - "apiKeysPermissionsUpdated": "Permissies bijgewerkt", - "apiKeysPermissionsUpdatedDescription": "De bevoegdheden zijn bijgewerkt.", - "apiKeysPermissionsGeneralSettings": "Machtigingen", - "apiKeysPermissionsGeneralSettingsDescription": "Bepaal wat deze API-sleutel kan doen", - "apiKeysPermissionsSave": "Rechten opslaan", - "apiKeysPermissionsTitle": "Machtigingen", - "apiKeys": "API sleutels", - "searchApiKeys": "API-sleutels zoeken...", - "apiKeysAdd": "API-sleutel genereren", - "apiKeysErrorDelete": "Fout bij verwijderen API-sleutel", - "apiKeysErrorDeleteMessage": "Fout bij verwijderen API-sleutel", - "apiKeysQuestionRemove": "Weet u zeker dat u de API-sleutel {selectedApiKey} van de organisatie wilt verwijderen?", - "apiKeysMessageRemove": "Eenmaal verwijderd, kan de API-sleutel niet meer worden gebruikt.", - "apiKeysMessageConfirm": "Om dit te bevestigen, typt u de naam van de API-sleutel hieronder.", - "apiKeysDeleteConfirm": "Bevestig Verwijderen API-sleutel", - "apiKeysDelete": "API-sleutel verwijderen", - "apiKeysManage": "API-sleutels beheren", - "apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API", - "apiKeysSettings": "{apiKeyName} instellingen", - "userTitle": "Alle gebruikers beheren", - "userDescription": "Bekijk en beheer alle gebruikers in het systeem", - "userAbount": "Over gebruikersbeheer", - "userAbountDescription": "Deze tabel toont alle root user objecten in het systeem. Elke gebruiker kan tot meerdere organisaties behoren. Een gebruiker verwijderen uit een organisatie verwijdert hun root gebruiker object niet - ze zullen in het systeem blijven. Om een gebruiker volledig te verwijderen uit het systeem, moet u hun root gebruiker object verwijderen met behulp van de actie in deze tabel.", - "userServer": "Server Gebruikers", - "userSearch": "Zoek server gebruikers...", - "userErrorDelete": "Fout bij verwijderen gebruiker", - "userDeleteConfirm": "Bevestig verwijderen gebruiker", - "userDeleteServer": "Gebruiker verwijderen van de server", - "userMessageRemove": "De gebruiker zal uit alle organisaties verwijderd worden en volledig verwijderd worden van de server.", - "userMessageConfirm": "Typ de naam van de gebruiker hieronder om te bevestigen.", - "userQuestionRemove": "Weet je zeker dat je {selectedUser} permanent van de server wilt verwijderen?", - "licenseKey": "Licentie sleutel", - "valid": "Geldig", - "numberOfSites": "Aantal sites", - "licenseKeySearch": "Licentiesleutels zoeken...", - "licenseKeyAdd": "Licentiesleutel toevoegen", - "type": "Type", - "licenseKeyRequired": "Licentiesleutel is vereist", - "licenseTermsAgree": "U moet akkoord gaan met de licentievoorwaarden", - "licenseErrorKeyLoad": "Laden van licentiesleutels mislukt", - "licenseErrorKeyLoadDescription": "Er is een fout opgetreden bij het laden van licentiecodes.", - "licenseErrorKeyDelete": "Licentiesleutel verwijderen mislukt", - "licenseErrorKeyDeleteDescription": "Er is een fout opgetreden bij het verwijderen van licentiesleutel.", - "licenseKeyDeleted": "Licentiesleutel verwijderd", - "licenseKeyDeletedDescription": "De licentiesleutel is verwijderd.", - "licenseErrorKeyActivate": "Licentiesleutel activeren mislukt", - "licenseErrorKeyActivateDescription": "Er is een fout opgetreden tijdens het activeren van de licentiesleutel.", - "licenseAbout": "Over licenties", - "communityEdition": "Community editie", - "licenseAboutDescription": "Dit geldt voor gebruikers van bedrijven en ondernemingen die Pangolin in gebruiken in een commerciële omgeving. Als u Pangolin gebruikt voor persoonlijk gebruik, kunt u dit gedeelte negeren.", - "licenseKeyActivated": "Licentiesleutel geactiveerd", - "licenseKeyActivatedDescription": "De licentiesleutel is geactiveerd.", - "licenseErrorKeyRecheck": "Kon licentiesleutels niet opnieuw controleren", - "licenseErrorKeyRecheckDescription": "Er is een fout opgetreden bij het opnieuw controleren van licentiecodes.", - "licenseErrorKeyRechecked": "Licentiesleutels opnieuw gecontroleerd", - "licenseErrorKeyRecheckedDescription": "Alle licentiesleutels zijn opnieuw gecontroleerd", - "licenseActivateKey": "Activeer licentiesleutel", - "licenseActivateKeyDescription": "Voer een licentiesleutel in om deze te activeren.", - "licenseActivate": "Licentie activeren", - "licenseAgreement": "Door dit selectievakje aan te vinken, bevestigt u dat u de licentievoorwaarden hebt gelezen en ermee akkoord gaat die overeenkomen met de rang die is gekoppeld aan uw licentiesleutel.", - "fossorialLicense": "Fossorial Commerciële licentie- en abonnementsvoorwaarden bekijken", - "licenseMessageRemove": "Dit zal de licentiesleutel en alle bijbehorende machtigingen verwijderen die hierdoor zijn verleend.", - "licenseMessageConfirm": "Typ de licentiesleutel hieronder om te bevestigen.", - "licenseQuestionRemove": "Weet u zeker dat u de licentiesleutel {selectedKey} wilt verwijderen?", - "licenseKeyDelete": "Licentiesleutel verwijderen", - "licenseKeyDeleteConfirm": "Bevestig verwijderen licentiesleutel", - "licenseTitle": "Licentiestatus beheren", - "licenseTitleDescription": "Bekijk en beheer licentiesleutels in het systeem", - "licenseHost": "Host Licentie", - "licenseHostDescription": "Beheer de belangrijkste licentiesleutel voor de host.", - "licensedNot": "Niet gelicentieerd", - "hostId": "Host-ID", - "licenseReckeckAll": "Alle sleutels opnieuw selecteren", - "licenseSiteUsage": "Websites gebruik", - "licenseSiteUsageDecsription": "Bekijk het aantal sites dat deze licentie gebruikt.", - "licenseNoSiteLimit": "Er is geen limiet op het aantal sites dat een ongelicentieerde host gebruikt.", - "licensePurchase": "Licentie kopen", - "licensePurchaseSites": "Extra sites kopen", - "licenseSitesUsedMax": "{usedSites} van {maxSites} sites gebruikt", - "licenseSitesUsed": "{count, plural, =0 {# locaties} one {# locatie} other {# locaties}} in het systeem.", - "licensePurchaseDescription": "Kies hoeveel sites je wilt {selectedMode, select, license {Koop een licentie. Je kunt later altijd meer sites toevoegen.} other {Voeg je bestaande licentie toe}}", - "licenseFee": "Licentie vergoeding", - "licensePriceSite": "Prijs per site", - "total": "Totaal", - "licenseContinuePayment": "Doorgaan naar betaling", - "pricingPage": "prijsaanduiding pagina", - "pricingPortal": "Inkoopportaal bekijken", - "licensePricingPage": "Bezoek voor de meest recente prijzen en kortingen, a.u.b. de ", - "invite": "Uitnodigingen", - "inviteRegenerate": "Uitnodiging opnieuw genereren", - "inviteRegenerateDescription": "Verwijder vorige uitnodiging en maak een nieuwe", - "inviteRemove": "Verwijder uitnodiging", - "inviteRemoveError": "Uitnodiging verwijderen mislukt", - "inviteRemoveErrorDescription": "Er is een fout opgetreden tijdens het verwijderen van de uitnodiging.", - "inviteRemoved": "Uitnodiging verwijderd", - "inviteRemovedDescription": "De uitnodiging voor {email} is verwijderd.", - "inviteQuestionRemove": "Weet u zeker dat u de uitnodiging {email} wilt verwijderen?", - "inviteMessageRemove": "Eenmaal verwijderd, zal deze uitnodiging niet meer geldig zijn. U kunt de gebruiker later altijd opnieuw uitnodigen.", - "inviteMessageConfirm": "Om dit te bevestigen, typ dan het e-mailadres van onderstaande uitnodiging.", - "inviteQuestionRegenerate": "Weet u zeker dat u de uitnodiging voor {email}opnieuw wilt genereren? Dit zal de vorige uitnodiging intrekken.", - "inviteRemoveConfirm": "Bevestig verwijderen uitnodiging", - "inviteRegenerated": "Uitnodiging opnieuw gegenereerd", - "inviteSent": "Een nieuwe uitnodiging is verstuurd naar {email}.", - "inviteSentEmail": "Stuur e-mail notificatie naar de gebruiker", - "inviteGenerate": "Er is een nieuwe uitnodiging aangemaakt voor {email}.", - "inviteDuplicateError": "Dubbele uitnodiging", - "inviteDuplicateErrorDescription": "Er bestaat al een uitnodiging voor deze gebruiker.", - "inviteRateLimitError": "Tarief limiet overschreden", - "inviteRateLimitErrorDescription": "U hebt de limiet van 3 regeneratie per uur overschreden. Probeer het later opnieuw.", - "inviteRegenerateError": "Kan uitnodiging niet opnieuw aanmaken", - "inviteRegenerateErrorDescription": "Fout opgetreden tijdens het opnieuw genereren van de uitnodiging.", - "inviteValidityPeriod": "Geldigheid periode", - "inviteValidityPeriodSelect": "Geldigheid kiezen", - "inviteRegenerateMessage": "De uitnodiging is opnieuw gegenereerd. De gebruiker moet toegang krijgen tot de link hieronder om de uitnodiging te accepteren.", - "inviteRegenerateButton": "Hergenereren", - "expiresAt": "Verloopt op", - "accessRoleUnknown": "Onbekende rol", - "placeholder": "Plaatsaanduiding", - "userErrorOrgRemove": "Kan gebruiker niet verwijderen", - "userErrorOrgRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de gebruiker.", - "userOrgRemoved": "Gebruiker verwijderd", - "userOrgRemovedDescription": "De gebruiker {email} is verwijderd uit de organisatie.", - "userQuestionOrgRemove": "Weet u zeker dat u {email} wilt verwijderen uit de organisatie?", - "userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.", - "userMessageOrgConfirm": "Typ om te bevestigen de naam van de gebruiker hieronder.", - "userRemoveOrgConfirm": "Bevestig verwijderen gebruiker", - "userRemoveOrg": "Gebruiker uit organisatie verwijderen", - "users": "Gebruikers", - "accessRoleMember": "Lid", - "accessRoleOwner": "Eigenaar", - "userConfirmed": "Bevestigd", - "idpNameInternal": "Intern", - "emailInvalid": "Ongeldig e-mailadres", - "inviteValidityDuration": "Selecteer een tijdsduur", - "accessRoleSelectPlease": "Selecteer een rol", - "usernameRequired": "Gebruikersnaam is verplicht", - "idpSelectPlease": "Selecteer een identiteitsprovider", - "idpGenericOidc": "Algemene OAuth2/OIDC provider.", - "accessRoleErrorFetch": "Rollen ophalen mislukt", - "accessRoleErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de rollen", - "idpErrorFetch": "Kan identiteitsaanbieders niet ophalen", - "idpErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van identiteitsproviders", - "userErrorExists": "Gebruiker bestaat al", - "userErrorExistsDescription": "Deze gebruiker is al lid van de organisatie.", - "inviteError": "Uitnodigen van gebruiker mislukt", - "inviteErrorDescription": "Er is een fout opgetreden tijdens het uitnodigen van de gebruiker", - "userInvited": "Gebruiker uitgenodigd", - "userInvitedDescription": "De gebruiker is succesvol uitgenodigd.", - "userErrorCreate": "Gebruiker aanmaken mislukt", - "userErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van de gebruiker", - "userCreated": "Gebruiker aangemaakt", - "userCreatedDescription": "De gebruiker is succesvol aangemaakt.", - "userTypeInternal": "Interne gebruiker", - "userTypeInternalDescription": "Nodig een gebruiker uit om direct lid te worden van je organisatie.", - "userTypeExternal": "Externe gebruiker", - "userTypeExternalDescription": "Maak een gebruiker aan met een externe identiteitsprovider.", - "accessUserCreateDescription": "Volg de onderstaande stappen om een nieuwe gebruiker te maken", - "userSeeAll": "Alle gebruikers bekijken", - "userTypeTitle": "Type gebruiker", - "userTypeDescription": "Bepaal hoe u de gebruiker wilt aanmaken", - "userSettings": "Gebruikers informatie", - "userSettingsDescription": "Voer de gegevens van de nieuwe gebruiker in", - "inviteEmailSent": "Stuur uitnodigingsmail naar de gebruiker", - "inviteValid": "Geldig voor", - "selectDuration": "Selecteer duur", - "accessRoleSelect": "Selecteer rol", - "inviteEmailSentDescription": "Een e-mail is verstuurd naar de gebruiker met de link hieronder. Ze moeten toegang krijgen tot de link om de uitnodiging te accepteren.", - "inviteSentDescription": "De gebruiker is uitgenodigd. Ze moeten toegang krijgen tot de link hieronder om de uitnodiging te accepteren.", - "inviteExpiresIn": "De uitnodiging vervalt over {days, plural, one {# dag} other {# dagen}}.", - "idpTitle": "Identiteit Provider", - "idpSelect": "Identiteitsprovider voor de externe gebruiker selecteren", - "idpNotConfigured": "Er zijn geen identiteitsproviders geconfigureerd. Configureer een identiteitsprovider voordat u externe gebruikers aanmaakt.", - "usernameUniq": "Dit moet overeenkomen met de unieke gebruikersnaam die bestaat in de geselecteerde identiteitsprovider.", - "emailOptional": "E-mailadres (optioneel)", - "nameOptional": "Naam (optioneel)", - "accessControls": "Toegang Bediening", - "userDescription2": "Beheer de instellingen van deze gebruiker", - "accessRoleErrorAdd": "Gebruiker aan rol toevoegen mislukt", - "accessRoleErrorAddDescription": "Er is een fout opgetreden tijdens het toevoegen van de rol.", - "userSaved": "Gebruiker opgeslagen", - "userSavedDescription": "De gebruiker is bijgewerkt.", - "autoProvisioned": "Automatisch bevestigen", - "autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider", - "accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie", - "accessControlsSubmit": "Bewaar Toegangsbesturing", - "roles": "Rollen", - "accessUsersRoles": "Beheer Gebruikers & Rollen", - "accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot uw organisatie te beheren", - "key": "Sleutel", - "createdAt": "Aangemaakt op", - "proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.", - "proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.", - "proxyEnableSSL": "SSL inschakelen", - "proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.", - "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", - "siteErrorFetchDescription": "Er is een fout opgetreden tijdens het ophalen van het document", - "targetErrorDuplicate": "Dubbel doelwit", - "targetErrorDuplicateDescription": "Een doel met deze instellingen bestaat al", - "targetWireGuardErrorInvalidIp": "Ongeldig doel-IP", - "targetWireGuardErrorInvalidIpDescription": "Doel IP moet binnen de site subnet zijn", - "targetsUpdated": "Doelstellingen bijgewerkt", - "targetsUpdatedDescription": "Doelstellingen en instellingen succesvol bijgewerkt", - "targetsErrorUpdate": "Kan doelen niet bijwerken", - "targetsErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de doelen", - "targetTlsUpdate": "TLS instellingen bijgewerkt", - "targetTlsUpdateDescription": "Uw TLS instellingen zijn succesvol bijgewerkt", - "targetErrorTlsUpdate": "Bijwerken van TLS instellingen mislukt", - "targetErrorTlsUpdateDescription": "Fout opgetreden tijdens het bijwerken van de TLS-instellingen", - "proxyUpdated": "Proxyinstellingen bijgewerkt", - "proxyUpdatedDescription": "Uw proxyinstellingen zijn succesvol bijgewerkt", - "proxyErrorUpdate": "Bijwerken van proxy-instellingen mislukt", - "proxyErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de proxy-instellingen", - "targetAddr": "IP / Hostnaam", - "targetPort": "Poort", - "targetProtocol": "Protocol", - "targetTlsSettings": "HTTPS & TLS instellingen", - "targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren", - "targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen", - "targetTlsSni": "TLS servernaam", - "targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.", - "targetTlsSubmit": "Instellingen opslaan", - "targets": "Doelstellingen configuratie", - "targetsDescription": "Stel doelen in om verkeer naar uw backend-services te leiden", - "targetStickySessions": "Sticky sessies inschakelen", - "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", - "methodSelect": "Selecteer methode", - "targetSubmit": "Doelwit toevoegen", - "targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.", - "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", - "proxyCustomHeaderDescription": "De hostkop om in te stellen bij proxying verzoeken. Laat leeg om de standaard te gebruiken.", - "proxyAdditionalSubmit": "Proxyinstellingen opslaan", - "subnetMaskErrorInvalid": "Ongeldig subnet masker. Moet tussen 0 en 32 zijn.", - "ipAddressErrorInvalidFormat": "Ongeldig IP-adresformaat", - "ipAddressErrorInvalidOctet": "Ongeldige IP adres octet", - "path": "Pad", - "matchPath": "Overeenkomend pad", - "ipAddressRange": "IP Bereik", - "rulesErrorFetch": "Regels ophalen mislukt", - "rulesErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de regels", - "rulesErrorDuplicate": "Dupliceer regel", - "rulesErrorDuplicateDescription": "Een regel met deze instellingen bestaat al", - "rulesErrorInvalidIpAddressRange": "Ongeldige CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Voer een geldige CIDR waarde in", - "rulesErrorInvalidUrl": "Ongeldige URL pad", - "rulesErrorInvalidUrlDescription": "Voer een geldige URL padwaarde in", - "rulesErrorInvalidIpAddress": "Ongeldig IP", - "rulesErrorInvalidIpAddressDescription": "Voer een geldig IP-adres in", - "rulesErrorUpdate": "Regels bijwerken mislukt", - "rulesErrorUpdateDescription": "Fout opgetreden tijdens het bijwerken van de regels", - "rulesUpdated": "Regels inschakelen", - "rulesUpdatedDescription": "Regel evaluatie is bijgewerkt", - "rulesMatchIpAddressRangeDescription": "Voer een adres in in het CIDR-formaat (bijv. 103.21.244.0/22)", - "rulesMatchIpAddress": "Voer een IP-adres in (bijv. 103.21.244.12)", - "rulesMatchUrl": "Voer een URL-pad of patroon in (bijv. /api/v1/todos of /api/v1/*)", - "rulesErrorInvalidPriority": "Ongeldige prioriteit", - "rulesErrorInvalidPriorityDescription": "Voer een geldige prioriteit in", - "rulesErrorDuplicatePriority": "Dubbele prioriteiten", - "rulesErrorDuplicatePriorityDescription": "Voer unieke prioriteiten in", - "ruleUpdated": "Regels bijgewerkt", - "ruleUpdatedDescription": "Regels met succes bijgewerkt", - "ruleErrorUpdate": "Bewerking mislukt", - "ruleErrorUpdateDescription": "Er is een fout opgetreden tijdens het opslaan", - "rulesPriority": "Prioriteit", - "rulesAction": "actie", - "rulesMatchType": "Wedstrijd Type", - "value": "Waarde", - "rulesAbout": "Over regels", - "rulesAboutDescription": "Regels stellen u in staat om de toegang tot uw bron te controleren op basis van een aantal criteria. U kunt regels maken om toegang te toestaan of weigeren op basis van IP-adres of URL pad.", - "rulesActions": "acties", - "rulesActionAlwaysAllow": "Altijd toegestaan: Omzeil alle authenticatiemethoden", - "rulesActionAlwaysDeny": "Altijd weigeren: Blokkeer alle aanvragen, er kan geen verificatie worden geprobeerd", - "rulesActionPassToAuth": "Doorgeven aan Auth: Toestaan dat authenticatiemethoden worden geprobeerd", - "rulesMatchCriteria": "Overeenkomende criteria", - "rulesMatchCriteriaIpAddress": "Overeenkomen met een specifiek IP-adres", - "rulesMatchCriteriaIpAddressRange": "Overeenkomen met een bereik van IP-adressen in de CIDR-notatie", - "rulesMatchCriteriaUrl": "Koppel een URL-pad of patroon", - "rulesEnable": "Regels inschakelen", - "rulesEnableDescription": "In- of uitschakelen van regelevaluatie voor deze bron", - "rulesResource": "Configuratie Resource Regels", - "rulesResourceDescription": "Regels instellen om toegang tot uw bron te beheren", - "ruleSubmit": "Regel toevoegen", - "rulesNoOne": "Geen regels. Voeg een regel toe via het formulier.", - "rulesOrder": "Regels worden in oplopende volgorde volgens prioriteit beoordeeld.", - "rulesSubmit": "Regels opslaan", - "resourceErrorCreate": "Fout bij maken document", - "resourceErrorCreateDescription": "Er is een fout opgetreden bij het maken van het document", - "resourceErrorCreateMessage": "Fout bij maken bron:", - "resourceErrorCreateMessageDescription": "Er is een onverwachte fout opgetreden", - "sitesErrorFetch": "Fout bij ophalen sites", - "sitesErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de sites", - "domainsErrorFetch": "Fout bij ophalen domeinen", - "domainsErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de domeinen", - "none": "geen", - "unknown": "onbekend", - "resources": "Bronnen", - "resourcesDescription": "Bronnen zijn proxies voor applicaties die op uw privénetwerk worden uitgevoerd. Maak een bron aan voor elke HTTP/HTTPS of onbewerkte TCP/UDP-service op uw privénetwerk. Elke bron moet verbonden zijn met een site om private, beveiligde verbinding mogelijk te maken via een versleutelde WireGuard tunnel.", - "resourcesWireGuardConnect": "Beveiligde verbinding met WireGuard versleuteling", - "resourcesMultipleAuthenticationMethods": "Meerdere verificatiemethoden configureren", - "resourcesUsersRolesAccess": "Gebruiker en rol-gebaseerde toegangsbeheer", - "resourcesErrorUpdate": "Bron wisselen mislukt", - "resourcesErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document", - "access": "Toegangsrechten", - "shareLink": "{resource} Share link", - "resourceSelect": "Selecteer resource", - "shareLinks": "Links delen", - "share": "Deelbare links", - "shareDescription2": "Maak deelbare links naar uw bronnen. Links bieden tijdelijke of onbeperkte toegang tot uw bron. U kunt de vervalduur van de link configureren wanneer u er een aanmaakt.", - "shareEasyCreate": "Makkelijk te maken en te delen", - "shareConfigurableExpirationDuration": "Configureerbare vervalduur", - "shareSecureAndRevocable": "Veilig en herroepbaar", - "nameMin": "De naam moet minstens {len} tekens bevatten.", - "nameMax": "Naam mag niet langer zijn dan {len} tekens.", - "sitesConfirmCopy": "Bevestig dat u de configuratie hebt gekopieerd.", - "unknownCommand": "Onbekende opdracht", - "newtErrorFetchReleases": "Kan release-informatie niet ophalen: {err}", - "newtErrorFetchLatest": "Fout bij ophalen van laatste release: {err}", - "newtEndpoint": "Newt Eindoordeel", - "newtId": "Newt-ID", - "newtSecretKey": "Nieuwe geheime sleutel", - "architecture": "Architectuur", - "sites": "Sites", - "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.", - "siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients", - "siteWgManualConfigurationRequired": "Handmatige configuratie vereist", - "userErrorNotAdminOrOwner": "Gebruiker is geen beheerder of eigenaar", - "pangolinSettings": "Instellingen - Pangolin", - "accessRoleYour": "Jouw rol:", - "accessRoleSelect2": "Selecteer lidmaatschap", - "accessUserSelect": "Selecteer een gebruiker", - "otpEmailEnter": "Voer e-mailadres in", - "otpEmailEnterDescription": "Druk op enter om een e-mail toe te voegen na het typen in het invoerveld.", - "otpEmailErrorInvalid": "Ongeldig e-mailadres. Wildcard (*) moet het hele lokale deel zijn.", - "otpEmailSmtpRequired": "SMTP vereist", - "otpEmailSmtpRequiredDescription": "SMTP moet ingeschakeld zijn op de server om eenmalige wachtwoordauthenticatie te gebruiken.", - "otpEmailTitle": "Eenmalige wachtwoorden", - "otpEmailTitleDescription": "Vereis e-mailgebaseerde authenticatie voor brontoegang", - "otpEmailWhitelist": "E-mail whitelist", - "otpEmailWhitelistList": "Toegestane e-mails", - "otpEmailWhitelistListDescription": "Alleen gebruikers met deze e-mailadressen hebben toegang tot dit document. Ze zullen worden gevraagd om een eenmalig wachtwoord in te voeren dat naar hun e-mail is verzonden. Wildcards (*@example.com) kunnen worden gebruikt om elk e-mailadres van een domein toe te staan.", - "otpEmailWhitelistSave": "Whitelist opslaan", - "passwordAdd": "Wachtwoord toevoegen", - "passwordRemove": "Wachtwoord verwijderen", - "pincodeAdd": "PIN-code toevoegen", - "pincodeRemove": "PIN-code verwijderen", - "resourceAuthMethods": "Authenticatie methoden", - "resourceAuthMethodsDescriptions": "Sta toegang tot de bron toe via extra autorisatiemethoden", - "resourceAuthSettingsSave": "Succesvol opgeslagen", - "resourceAuthSettingsSaveDescription": "Verificatie-instellingen zijn opgeslagen", - "resourceErrorAuthFetch": "Gegevens ophalen mislukt", - "resourceErrorAuthFetchDescription": "Er is een fout opgetreden bij het ophalen van de gegevens", - "resourceErrorPasswordRemove": "Fout bij verwijderen resource wachtwoord", - "resourceErrorPasswordRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van het bronwachtwoord", - "resourceErrorPasswordSetup": "Fout bij instellen resource wachtwoord", - "resourceErrorPasswordSetupDescription": "Er is een fout opgetreden bij het instellen van het wachtwoord bron", - "resourceErrorPincodeRemove": "Fout bij verwijderen resource pincode", - "resourceErrorPincodeRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de bronpincode", - "resourceErrorPincodeSetup": "Fout bij instellen resource PIN code", - "resourceErrorPincodeSetupDescription": "Er is een fout opgetreden bij het instellen van de PIN-code van de bron", - "resourceErrorUsersRolesSave": "Kan rollen niet instellen", - "resourceErrorUsersRolesSaveDescription": "Er is een fout opgetreden tijdens het instellen van de rollen", - "resourceErrorWhitelistSave": "Kan whitelist niet opslaan", - "resourceErrorWhitelistSaveDescription": "Er is een fout opgetreden tijdens het opslaan van de whitelist", - "resourcePasswordSubmit": "Wachtwoordbescherming inschakelen", - "resourcePasswordProtection": "Wachtwoordbescherming {status}", - "resourcePasswordRemove": "Wachtwoord document verwijderd", - "resourcePasswordRemoveDescription": "Het wachtwoord van de resource is met succes verwijderd", - "resourcePasswordSetup": "Wachtwoord document ingesteld", - "resourcePasswordSetupDescription": "Het wachtwoord voor de bron is succesvol ingesteld", - "resourcePasswordSetupTitle": "Wachtwoord instellen", - "resourcePasswordSetupTitleDescription": "Stel een wachtwoord in om deze bron te beschermen", - "resourcePincode": "Pincode", - "resourcePincodeSubmit": "PIN-Code bescherming inschakelen", - "resourcePincodeProtection": "PIN Code bescherming {status}", - "resourcePincodeRemove": "Pijncode van resource verwijderd", - "resourcePincodeRemoveDescription": "Het wachtwoord van de resource is met succes verwijderd", - "resourcePincodeSetup": "PIN-code voor hulpbron ingesteld", - "resourcePincodeSetupDescription": "De bronpincode is succesvol ingesteld", - "resourcePincodeSetupTitle": "Pincode instellen", - "resourcePincodeSetupTitleDescription": "Stel een pincode in om deze hulpbron te beschermen", - "resourceRoleDescription": "Beheerders hebben altijd toegang tot deze bron.", - "resourceUsersRoles": "Gebruikers & Rollen", - "resourceUsersRolesDescription": "Configureer welke gebruikers en rollen deze pagina kunnen bezoeken", - "resourceUsersRolesSubmit": "Gebruikers opslaan & rollen", - "resourceWhitelistSave": "Succesvol opgeslagen", - "resourceWhitelistSaveDescription": "Whitelist instellingen zijn opgeslagen", - "ssoUse": "Gebruik Platform SSO", - "ssoUseDescription": "Bestaande gebruikers hoeven slechts eenmaal in te loggen voor alle bronnen die dit ingeschakeld hebben.", - "proxyErrorInvalidPort": "Ongeldig poortnummer", - "subdomainErrorInvalid": "Ongeldig subdomein", - "domainErrorFetch": "Fout bij ophalen domeinen", - "domainErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de domeinen", - "resourceErrorUpdate": "Bijwerken van resource mislukt", - "resourceErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document", - "resourceUpdated": "Bron bijgewerkt", - "resourceUpdatedDescription": "Het document is met succes bijgewerkt", - "resourceErrorTransfer": "Mislukt om resource over te dragen", - "resourceErrorTransferDescription": "Er is een fout opgetreden tijdens het overzetten van het document", - "resourceTransferred": "Bron overgedragen", - "resourceTransferredDescription": "De bron is met succes overgedragen.", - "resourceErrorToggle": "Bron wisselen mislukt", - "resourceErrorToggleDescription": "Er is een fout opgetreden tijdens het bijwerken van het document", - "resourceVisibilityTitle": "Zichtbaarheid", - "resourceVisibilityTitleDescription": "Zichtbaarheid van bestanden volledig in- of uitschakelen", - "resourceGeneral": "Algemene instellingen", - "resourceGeneralDescription": "Configureer de algemene instellingen voor deze bron", - "resourceEnable": "Resource inschakelen", - "resourceTransfer": "Bronnen overdragen", - "resourceTransferDescription": "Verplaats dit document naar een andere site", - "resourceTransferSubmit": "Bronnen overdragen", - "siteDestination": "Bestemming site", - "searchSites": "Sites zoeken", - "accessRoleCreate": "Rol aanmaken", - "accessRoleCreateDescription": "Maak een nieuwe rol aan om gebruikers te groeperen en hun rechten te beheren.", - "accessRoleCreateSubmit": "Rol aanmaken", - "accessRoleCreated": "Rol aangemaakt", - "accessRoleCreatedDescription": "De rol is succesvol aangemaakt.", - "accessRoleErrorCreate": "Rol aanmaken mislukt", - "accessRoleErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van de rol.", - "accessRoleErrorNewRequired": "Nieuwe rol is vereist", - "accessRoleErrorRemove": "Rol verwijderen mislukt", - "accessRoleErrorRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de rol.", - "accessRoleName": "Rol naam", - "accessRoleQuestionRemove": "U staat op het punt de {name} rol te verwijderen. U kunt deze actie niet ongedaan maken.", - "accessRoleRemove": "Rol verwijderen", - "accessRoleRemoveDescription": "Verwijder een rol van de organisatie", - "accessRoleRemoveSubmit": "Rol verwijderen", - "accessRoleRemoved": "Rol verwijderd", - "accessRoleRemovedDescription": "De rol is succesvol verwijderd.", - "accessRoleRequiredRemove": "Voordat u deze rol verwijdert, selecteer een nieuwe rol om bestaande leden aan te dragen.", - "manage": "Beheren", - "sitesNotFound": "Geen sites gevonden.", - "pangolinServerAdmin": "Serverbeheer - Pangolin", - "licenseTierProfessional": "Professionele licentie", - "licenseTierEnterprise": "Enterprise Licentie", - "licenseTierPersonal": "Personal License", - "licensed": "Gelicentieerd", - "yes": "ja", - "no": "Neen", - "sitesAdditional": "Extra sites", - "licenseKeys": "Licentie Sleutels", - "sitestCountDecrease": "Verlaag het aantal sites", - "sitestCountIncrease": "Toename van site vergroten", - "idpManage": "Identiteitsaanbieders beheren", - "idpManageDescription": "Identiteitsaanbieders in het systeem bekijken en beheren", - "idpDeletedDescription": "Identity provider succesvol verwijderd", - "idpOidc": "OAuth2/OIDC", - "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 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 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": "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": "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", - "idpTokenUrlDescription": "De URL van het OAuth2 token eindpunt", - "idpOidcConfigureAlert": "Belangrijke informatie", - "idpOidcConfigureAlertDescription": "Na het aanmaken van de identity provider moet u de callback URL configureren in de instellingen van uw identity provider. De callback URL zal worden opgegeven na het succesvol aanmaken.", - "idpToken": "Token configuratie", - "idpTokenDescription": "Stel in hoe gebruikersgegevens uit het ID token uit te pakken", - "idpJmespathAbout": "Over JMESPath", - "idpJmespathAboutDescription": "De onderstaande paden gebruiken JMESPath syntaxis om waarden van de ID-token te extraheren.", - "idpJmespathAboutDescriptionLink": "Meer informatie over JMESPath", - "idpJmespathLabel": "ID pad", - "idpJmespathLabelDescription": "Het pad naar het gebruiker-id in het ID-token", - "idpJmespathEmailPathOptional": "E-mail pad (optioneel)", - "idpJmespathEmailPathOptionalDescription": "Het pad naar het e-mailadres van de gebruiker in het ID-token", - "idpJmespathNamePathOptional": "Naam pad (optioneel)", - "idpJmespathNamePathOptionalDescription": "Het pad naar de naam van de gebruiker in de ID-token", - "idpOidcConfigureScopes": "Toepassingsgebieden", - "idpOidcConfigureScopesDescription": "Te vragen ruimtescheiden lijst van OAuth2 toepassingsgebieden", - "idpSubmit": "Identity Provider aanmaken", - "orgPolicies": "Organisatie beleid", - "idpSettings": "{idpName} instellingen", - "idpCreateSettingsDescription": "Configureer de instellingen voor uw identiteitsprovider", - "roleMapping": "Rol Toewijzing", - "orgMapping": "Organisatie toewijzing", - "orgPoliciesSearch": "Zoek het organisatiebeleid...", - "orgPoliciesAdd": "Organisatiebeleid toevoegen", - "orgRequired": "Organisatie is vereist", - "error": "Foutmelding", - "success": "Geslaagd", - "orgPolicyAddedDescription": "Beleid succesvol toegevoegd", - "orgPolicyUpdatedDescription": "Beleid succesvol bijgewerkt", - "orgPolicyDeletedDescription": "Beleid succesvol verwijderd", - "defaultMappingsUpdatedDescription": "Standaard toewijzingen met succes bijgewerkt", - "orgPoliciesAbout": "Over organisatiebeleid", - "orgPoliciesAboutDescription": "Organisatiebeleid wordt gebruikt om toegang tot organisaties te beheren op basis van de gebruiker-ID-token. U kunt JMESPath expressies opgeven om rol en organisatie informatie van de ID-token te extraheren.", - "orgPoliciesAboutDescriptionLink": "Zie documentatie, voor meer informatie.", - "defaultMappingsOptional": "Standaard toewijzingen (optioneel)", - "defaultMappingsOptionalDescription": "De standaard toewijzingen worden gebruikt wanneer er geen organisatiebeleid is gedefinieerd voor een organisatie. Je kunt de standaard rol en organisatietoewijzingen opgeven waar je naar terug kunt vallen.", - "defaultMappingsRole": "Standaard Rol Toewijzing", - "defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.", - "defaultMappingsOrg": "Standaard organisatie mapping", - "defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.", - "defaultMappingsSubmit": "Standaard toewijzingen opslaan", - "orgPoliciesEdit": "Organisatie beleid bewerken", - "org": "Organisatie", - "orgSelect": "Selecteer organisatie", - "orgSearch": "Zoek in org", - "orgNotFound": "Geen org gevonden.", - "roleMappingPathOptional": "Rol toewijzing pad (optioneel)", - "orgMappingPathOptional": "Organisatie mapping pad (optioneel)", - "orgPolicyUpdate": "Update beleid", - "orgPolicyAdd": "Beleid toevoegen", - "orgPolicyConfig": "Toegang voor een organisatie configureren", - "idpUpdatedDescription": "Identity provider succesvol bijgewerkt", - "redirectUrl": "Omleidings URL", - "redirectUrlAbout": "Over omleidings-URL", - "redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in uw identiteitsprovider-instellingen.", - "pangolinAuth": "Authenticatie - Pangolin", - "verificationCodeLengthRequirements": "Je verificatiecode moet 8 tekens bevatten.", - "errorOccurred": "Er is een fout opgetreden", - "emailErrorVerify": "E-mail verifiëren is mislukt:", - "emailVerified": "E-mail met succes geverifieerd! Doorsturen naar u...", - "verificationCodeErrorResend": "Fout bij het opnieuw verzenden van de verificatiecode:", - "verificationCodeResend": "Verificatiecode opnieuw verzonden", - "verificationCodeResendDescription": "We hebben een verificatiecode opnieuw naar je e-mailadres gestuurd. Controleer je inbox.", - "emailVerify": "Bevestig e-mailadres", - "emailVerifyDescription": "Voer de verificatiecode in die naar uw e-mailadres is verzonden.", - "verificationCode": "Verificatie Code", - "verificationCodeEmailSent": "We hebben een verificatiecode naar je e-mailadres gestuurd.", - "submit": "Bevestigen", - "emailVerifyResendProgress": "Opnieuw verzenden...", - "emailVerifyResend": "Geen code ontvangen? Klik hier om opnieuw te verzenden", - "passwordNotMatch": "Wachtwoorden komen niet overeen", - "signupError": "Er is een fout opgetreden tijdens het aanmelden", - "pangolinLogoAlt": "Pangolin logo", - "inviteAlready": "Het lijkt erop dat je bent uitgenodigd!", - "inviteAlreadyDescription": "Om de uitnodiging te accepteren, moet je inloggen of een account aanmaken.", - "signupQuestion": "Heeft u al een account?", - "login": "Inloggen", - "resourceNotFound": "Bron niet gevonden", - "resourceNotFoundDescription": "De bron die u probeert te benaderen bestaat niet.", - "pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn", - "pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten", - "passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn", - "passwordRequirementsTitle": "Wachtwoordvereisten:", - "passwordRequirementLength": "Minstens 8 tekens lang", - "passwordRequirementUppercase": "Minstens één hoofdletter", - "passwordRequirementLowercase": "Minstens één kleine letter", - "passwordRequirementNumber": "Minstens één cijfer", - "passwordRequirementSpecial": "Minstens één speciaal teken", - "passwordRequirementsMet": "✓ Wachtwoord voldoet aan alle vereisten", - "passwordStrength": "Wachtwoord sterkte", - "passwordStrengthWeak": "Zwak", - "passwordStrengthMedium": "Gemiddeld", - "passwordStrengthStrong": "Sterk", - "passwordRequirements": "Vereisten:", - "passwordRequirementLengthText": "8+ tekens", - "passwordRequirementUppercaseText": "Hoofdletter (A-Z)", - "passwordRequirementLowercaseText": "Kleine letter (a-z)", - "passwordRequirementNumberText": "Cijfer (0-9)", - "passwordRequirementSpecialText": "Speciaal teken (!@#$%...)", - "passwordsDoNotMatch": "Wachtwoorden komen niet overeen", - "otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn", - "otpEmailSent": "OTP verzonden", - "otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden", - "otpEmailErrorAuthenticate": "Authenticatie met e-mail mislukt", - "pincodeErrorAuthenticate": "Authenticatie met pincode mislukt", - "passwordErrorAuthenticate": "Authenticatie met wachtwoord mislukt", - "poweredBy": "Mogelijk gemaakt door", - "authenticationRequired": "Authenticatie vereist", - "authenticationMethodChoose": "Kies uw voorkeursmethode voor toegang tot {name}", - "authenticationRequest": "U moet zich aanmelden om {name} te kunnen gebruiken", - "user": "Gebruiker", - "pincodeInput": "6-cijferige PIN-Code", - "pincodeSubmit": "Inloggen met PIN", - "passwordSubmit": "Log in met wachtwoord", - "otpEmailDescription": "Een eenmalige code zal worden verzonden naar deze e-mail.", - "otpEmailSend": "Verstuur éénmalige code", - "otpEmail": "Eenmalig wachtwoord (OTP)", - "otpEmailSubmit": "OTP inzenden", - "backToEmail": "Terug naar E-mail", - "noSupportKey": "Server draait zonder een supporter sleutel. Overweeg het project te ondersteunen!", - "accessDenied": "Toegang geweigerd", - "accessDeniedDescription": "U heeft geen toegang tot deze resource. Als dit een vergissing is, neem dan contact op met de beheerder.", - "accessTokenError": "Fout bij controleren toegangstoken", - "accessGranted": "Toegang verleend", - "accessUrlInvalid": "URL ongeldig", - "accessGrantedDescription": "Er is u toegang verleend tot deze resource. U wordt doorgestuurd...", - "accessUrlInvalidDescription": "Deze URL voor gedeelde toegang is ongeldig. Neem contact op met de documenteigenaar voor een nieuwe URL.", - "tokenInvalid": "Ongeldig token", - "pincodeInvalid": "Ongeldige code", - "passwordErrorRequestReset": "Verzoek om resetten mislukt:", - "passwordErrorReset": "Wachtwoord opnieuw instellen mislukt:", - "passwordResetSuccess": "Wachtwoord succesvol gereset! Terug naar inloggen...", - "passwordReset": "Wachtwoord opnieuw instellen", - "passwordResetDescription": "Volg de stappen om uw wachtwoord opnieuw in te stellen", - "passwordResetSent": "We sturen een wachtwoord reset code naar dit e-mailadres.", - "passwordResetCode": "Resetcode", - "passwordResetCodeDescription": "Controleer je e-mail voor de reset code.", - "passwordNew": "Nieuw wachtwoord", - "passwordNewConfirm": "Bevestig nieuw wachtwoord", - "pincodeAuth": "Authenticatiecode", - "pincodeSubmit2": "Code indienen", - "passwordResetSubmit": "Opnieuw instellen aanvragen", - "passwordBack": "Terug naar wachtwoord", - "loginBack": "Ga terug naar login", - "signup": "Registreer nu", - "loginStart": "Log in om te beginnen", - "idpOidcTokenValidating": "Valideer OIDC-token", - "idpOidcTokenResponse": "Valideer OIDC token antwoord", - "idpErrorOidcTokenValidating": "Fout bij valideren OIDC-token", - "idpConnectingTo": "Verbinden met {name}", - "idpConnectingToDescription": "Uw identiteit bevestigen", - "idpConnectingToProcess": "Verbinden...", - "idpConnectingToFinished": "Verbonden", - "idpErrorConnectingTo": "Er was een probleem bij het verbinden met {name}. Neem contact op met uw beheerder.", - "idpErrorNotFound": "IdP niet gevonden", - "inviteInvalid": "Ongeldige uitnodiging", - "inviteInvalidDescription": "Uitnodigingslink is ongeldig.", - "inviteErrorWrongUser": "Uitnodiging is niet voor deze gebruiker", - "inviteErrorUserNotExists": "Gebruiker bestaat niet. Maak eerst een account aan.", - "inviteErrorLoginRequired": "Je moet ingelogd zijn om een uitnodiging te accepteren", - "inviteErrorExpired": "De uitnodiging is mogelijk verlopen", - "inviteErrorRevoked": "De uitnodiging is mogelijk ingetrokken", - "inviteErrorTypo": "Er kan een typefout zijn in de uitnodigingslink", - "pangolinSetup": "Instellen - Pangolin", - "orgNameRequired": "Organisatienaam is vereist", - "orgIdRequired": "Organisatie-ID is vereist", - "orgErrorCreate": "Fout opgetreden tijdens het aanmaken org", - "pageNotFound": "Pagina niet gevonden", - "pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.", - "overview": "Overzicht.", - "home": "Startpagina", - "accessControl": "Toegangs controle", - "settings": "Instellingen", - "usersAll": "Alle gebruikers", - "license": "Licentie", - "pangolinDashboard": "Dashboard - Pangolin", - "noResults": "Geen resultaten gevonden.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Ingevoerde tags", - "tagsEnteredDescription": "Dit zijn de tags die u hebt ingevoerd.", - "tagsWarnCannotBeLessThanZero": "maxTags en minTags kunnen niet minder dan 0 zijn", - "tagsWarnNotAllowedAutocompleteOptions": "Tag niet toegestaan als per autocomplete opties", - "tagsWarnInvalid": "Ongeldige tag per validateTag", - "tagWarnTooShort": "Tag {tagText} is te kort", - "tagWarnTooLong": "Tag {tagText} is te lang", - "tagsWarnReachedMaxNumber": "Het maximum aantal toegestane tags bereikt", - "tagWarnDuplicate": "Dubbele tag {tagText} niet toegevoegd", - "supportKeyInvalid": "Ongeldige sleutel", - "supportKeyInvalidDescription": "Je supporter sleutel is ongeldig.", - "supportKeyValid": "Geldige sleutel", - "supportKeyValidDescription": "Uw supporter sleutel is gevalideerd. Bedankt voor uw steun!", - "supportKeyErrorValidationDescription": "Niet gelukt om de supportersleutel te valideren.", - "supportKey": "Ondersteun ontwikkeling en Adopt een Pangolin!", - "supportKeyDescription": "Koop een supporter sleutel om ons te helpen Pangolin voor de gemeenschap te blijven ontwikkelen. Je bijdrage geeft ons meer tijd om nieuwe functies te behouden en toe te voegen aan de applicatie voor iedereen. We zullen dit nooit gebruiken voor paywall-functies. Dit staat los van elke commerciële editie.", - "supportKeyPet": "U zult ook uw eigen huisdier Pangolin moeten adopteren en ontmoeten!", - "supportKeyPurchase": "Betalingen worden verwerkt via GitHub. Daarna kunt u de sleutel ophalen op", - "supportKeyPurchaseLink": "onze website", - "supportKeyPurchase2": "en verzilver het hier.", - "supportKeyLearnMore": "Meer informatie.", - "supportKeyOptions": "Selecteer de optie die het beste bij u past.", - "supportKetOptionFull": "Volledige supporter", - "forWholeServer": "Voor de hele server", - "lifetimePurchase": "Levenslange aankoop", - "supporterStatus": "Status supporter", - "buy": "Kopen", - "supportKeyOptionLimited": "Beperkte Supporter", - "forFiveUsers": "Voor 5 of minder gebruikers", - "supportKeyRedeem": "Supportersleutel inwisselen", - "supportKeyHideSevenDays": "Verbergen voor 7 dagen", - "supportKeyEnter": "Voer de supportersleutel in", - "supportKeyEnterDescription": "Ontmoet je eigen huisdier Pangolin!", - "githubUsername": "GitHub-gebruikersnaam", - "supportKeyInput": "Supporter Sleutel", - "supportKeyBuy": "Koop supportersleutel", - "logoutError": "Fout bij uitloggen", - "signingAs": "Ingelogd als", - "serverAdmin": "Server beheer", - "managedSelfhosted": "Beheerde Self-Hosted", - "otpEnable": "Twee-factor inschakelen", - "otpDisable": "Tweestapsverificatie uitschakelen", - "logout": "Log uit", - "licenseTierProfessionalRequired": "Professionele editie vereist", - "licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.", - "actionGetOrg": "Krijg Organisatie", - "updateOrgUser": "Org gebruiker bijwerken", - "createOrgUser": "Org gebruiker aanmaken", - "actionUpdateOrg": "Organisatie bijwerken", - "actionUpdateUser": "Gebruiker bijwerken", - "actionGetUser": "Gebruiker ophalen", - "actionGetOrgUser": "Krijg organisatie-gebruiker", - "actionListOrgDomains": "Lijst organisatie domeinen", - "actionCreateSite": "Site aanmaken", - "actionDeleteSite": "Site verwijderen", - "actionGetSite": "Site ophalen", - "actionListSites": "Sites weergeven", - "actionApplyBlueprint": "Blauwdruk toepassen", - "setupToken": "Instel Token", - "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", - "setupTokenRequired": "Setup-token is vereist", - "actionUpdateSite": "Site bijwerken", - "actionListSiteRoles": "Toon toegestane sitenollen", - "actionCreateResource": "Bron maken", - "actionDeleteResource": "Document verwijderen", - "actionGetResource": "Bron ophalen", - "actionListResource": "Bronnen weergeven", - "actionUpdateResource": "Document bijwerken", - "actionListResourceUsers": "Lijst van documentgebruikers", - "actionSetResourceUsers": "Stel document gebruikers in", - "actionSetAllowedResourceRoles": "Toegestane Resource Rollen instellen", - "actionListAllowedResourceRoles": "Lijst Toegestane Resource Rollen", - "actionSetResourcePassword": "Stel bronwachtwoord in", - "actionSetResourcePincode": "Stel Resource Pincode in", - "actionSetResourceEmailWhitelist": "Stel Resource e-mail whitelist in", - "actionGetResourceEmailWhitelist": "Verkrijg Resource E-mail Whitelist", - "actionCreateTarget": "Doelwit aanmaken", - "actionDeleteTarget": "Verwijder doel", - "actionGetTarget": "Verkrijg Doel", - "actionListTargets": "Doelstellingen weergeven", - "actionUpdateTarget": "Doelwit bijwerken", - "actionCreateRole": "Rol aanmaken", - "actionDeleteRole": "Verwijder rol", - "actionGetRole": "Krijg Rol", - "actionListRole": "Toon rollen", - "actionUpdateRole": "Rol bijwerken", - "actionListAllowedRoleResources": "Lijst toegestane rolbronnen", - "actionInviteUser": "Gebruiker uitnodigen", - "actionRemoveUser": "Gebruiker verwijderen", - "actionListUsers": "Gebruikers weergeven", - "actionAddUserRole": "Gebruikersrol toevoegen", - "actionGenerateAccessToken": "Genereer Toegangstoken", - "actionDeleteAccessToken": "Verwijder toegangstoken", - "actionListAccessTokens": "Lijst toegangstokens", - "actionCreateResourceRule": "Bronregel aanmaken", - "actionDeleteResourceRule": "Verwijder Resource Regel", - "actionListResourceRules": "Bron regels weergeven", - "actionUpdateResourceRule": "Bronregel bewerken", - "actionListOrgs": "Organisaties weergeven", - "actionCheckOrgId": "ID controleren", - "actionCreateOrg": "Nieuwe organisatie aanmaken", - "actionDeleteOrg": "Verwijder organisatie", - "actionListApiKeys": "API-sleutels weergeven", - "actionListApiKeyActions": "Lijst van API Key Acties", - "actionSetApiKeyActions": "Stel API Key Toegestane Acties", - "actionCreateApiKey": "API-sleutel aanmaken", - "actionDeleteApiKey": "API-sleutel verwijderen", - "actionCreateIdp": "IDP aanmaken", - "actionUpdateIdp": "IDP bijwerken", - "actionDeleteIdp": "Verwijder IDP", - "actionListIdps": "Toon IDP", - "actionGetIdp": "IDP ophalen", - "actionCreateIdpOrg": "Maak IDP Org Policy", - "actionDeleteIdpOrg": "Verwijder IDP Org Beleid", - "actionListIdpOrgs": "Toon IDP Orgs", - "actionUpdateIdpOrg": "IDP-org bijwerken", - "actionCreateClient": "Client aanmaken", - "actionDeleteClient": "Verwijder klant", - "actionUpdateClient": "Klant bijwerken", - "actionListClients": "Lijst klanten", - "actionGetClient": "Client ophalen", - "actionCreateSiteResource": "Sitebron maken", - "actionDeleteSiteResource": "Document verwijderen van site", - "actionGetSiteResource": "Bron van site ophalen", - "actionListSiteResources": "Bronnen van site weergeven", - "actionUpdateSiteResource": "Document bijwerken van site", - "actionListInvitations": "Toon uitnodigingen", - "noneSelected": "Niet geselecteerd", - "orgNotFound2": "Geen organisaties gevonden.", - "searchProgress": "Zoeken...", - "create": "Aanmaken", - "orgs": "Organisaties", - "loginError": "Er is een fout opgetreden tijdens het inloggen", - "passwordForgot": "Wachtwoord vergeten?", - "otpAuth": "Tweestapsverificatie verificatie", - "otpAuthDescription": "Voer de code van je authenticator-app of een van je reservekopiecodes voor het eenmalig gebruik in.", - "otpAuthSubmit": "Code indienen", - "idpContinue": "Of ga verder met", - "otpAuthBack": "Terug naar inloggen", - "navbar": "Navigatiemenu", - "navbarDescription": "Hoofd navigatie menu voor de applicatie", - "navbarDocsLink": "Documentatie", - "otpErrorEnable": "Kan 2FA niet inschakelen", - "otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA", - "otpSetupCheckCode": "Voer een 6-cijferige code in", - "otpSetupCheckCodeRetry": "Ongeldige code. Probeer het opnieuw.", - "otpSetup": "Tweestapsverificatie inschakelen", - "otpSetupDescription": "Beveilig je account met een extra beveiligingslaag", - "otpSetupScanQr": "Scan deze QR-code met je authenticator-app of voer de geheime sleutel handmatig in:", - "otpSetupSecretCode": "Authenticatiecode", - "otpSetupSuccess": "Tweestapsverificatie ingeschakeld", - "otpSetupSuccessStoreBackupCodes": "Uw account is nu veiliger. Vergeet niet uw back-upcodes op te slaan.", - "otpErrorDisable": "Kan 2FA niet uitschakelen", - "otpErrorDisableDescription": "Er is een fout opgetreden tijdens het uitschakelen van 2FA", - "otpRemove": "Tweestapsverificatie uitschakelen", - "otpRemoveDescription": "Tweestapsverificatie uitschakelen voor je account", - "otpRemoveSuccess": "Tweestapsverificatie uitgeschakeld", - "otpRemoveSuccessMessage": "Tweestapsverificatie is uitgeschakeld voor uw account. U kunt dit op elk gewenst moment opnieuw inschakelen.", - "otpRemoveSubmit": "2FA uitschakelen", - "paginator": "Pagina {current} van {last}", - "paginatorToFirst": "Ga naar eerste pagina", - "paginatorToPrevious": "Ga naar vorige pagina", - "paginatorToNext": "Ga naar de volgende pagina", - "paginatorToLast": "Ga naar de laatste pagina", - "copyText": "Tekst kopiëren", - "copyTextFailed": "Kan tekst niet kopiëren: ", - "copyTextClipboard": "Kopiëren naar klembord", - "inviteErrorInvalidConfirmation": "Ongeldige bevestiging", - "passwordRequired": "Wachtwoord is vereist", - "allowAll": "Alles toestaan", - "permissionsAllowAll": "Alle machtigingen toestaan", - "githubUsernameRequired": "GitHub gebruikersnaam is vereist", - "supportKeyRequired": "Supportersleutel is vereist", - "passwordRequirementsChars": "Wachtwoord moet ten minste 8 tekens bevatten", - "language": "Taal", - "verificationCodeRequired": "Code is vereist", - "userErrorNoUpdate": "Geen gebruiker om te updaten", - "siteErrorNoUpdate": "Geen site om bij te werken", - "resourceErrorNoUpdate": "Geen document om bij te werken", - "authErrorNoUpdate": "Geen authenticatie informatie om bij te werken", - "orgErrorNoUpdate": "Geen org om bij te werken", - "orgErrorNoProvided": "Geen org opgegeven", - "apiKeysErrorNoUpdate": "Geen API-sleutel om bij te werken", - "sidebarOverview": "Overzicht.", - "sidebarHome": "Startpagina", - "sidebarSites": "Werkruimtes", - "sidebarResources": "Bronnen", - "sidebarAccessControl": "Toegangs controle", - "sidebarUsers": "Gebruikers", - "sidebarInvitations": "Uitnodigingen", - "sidebarRoles": "Rollen", - "sidebarShareableLinks": "Deelbare links", - "sidebarApiKeys": "API sleutels", - "sidebarSettings": "Instellingen", - "sidebarAllUsers": "Alle gebruikers", - "sidebarIdentityProviders": "Identiteit aanbieders", - "sidebarLicense": "Licentie", - "sidebarClients": "Clients", - "sidebarDomains": "Domeinen", - "enableDockerSocket": "Schakel Docker Blauwdruk in", - "enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.", - "enableDockerSocketLink": "Meer informatie", - "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", - "containerImage": "Afbeelding", - "containerState": "Provincie", - "containerNetworks": "Netwerken", - "containerHostnameIp": "Hostnaam/IP", - "containerLabels": "Labels", - "containerLabelsCount": "{count, plural, one {# label} other {# labels}}", - "containerLabelsTitle": "Container labels", - "containerLabelEmpty": "", - "containerPorts": "Poorten", - "containerPortsMore": "+{count} meer", - "containerActions": "acties", - "select": "Selecteren", - "noContainersMatchingFilters": "Geen containers gevonden die overeenkomen met de huidige filters.", - "showContainersWithoutPorts": "Toon containers zonder poorten", - "showStoppedContainers": "Toon gestopte containers", - "noContainersFound": "Geen containers gevonden. Zorg ervoor dat Docker containers draaien.", - "searchContainersPlaceholder": "Zoek tussen {count} containers...", - "searchResultsCount": "{count, plural, one {# resultaat} other {# resultaten}}", - "filters": "Filters", - "filterOptions": "Filter opties", - "filterPorts": "Poorten", - "filterStopped": "Gestopt", - "clearAllFilters": "Alle filters wissen", - "columns": "Kolommen", - "toggleColumns": "Kolommen omschakelen", - "refreshContainersList": "Vernieuw containers lijst", - "searching": "Zoeken...", - "noContainersFoundMatching": "Geen containers gevonden die overeenkomen met \"{filter}\".", - "light": "licht", - "dark": "donker", - "system": "systeem", - "theme": "Thema", - "subnetRequired": "Subnet is vereist", - "initialSetupTitle": "Initiële serverconfiguratie", - "initialSetupDescription": "Maak het eerste serverbeheeraccount aan. Er kan slechts één serverbeheerder bestaan. U kunt deze inloggegevens later altijd wijzigen.", - "createAdminAccount": "Maak een beheeraccount aan", - "setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.", - "certificateStatus": "Certificaatstatus", - "loading": "Bezig met laden", - "restart": "Herstarten", - "domains": "Domeinen", - "domainsDescription": "Beheer domeinen voor je organisatie", - "domainsSearch": "Zoek domeinen...", - "domainAdd": "Domein toevoegen", - "domainAddDescription": "Registreer een nieuw domein bij je organisatie", - "domainCreate": "Domein aanmaken", - "domainCreatedDescription": "Domein succesvol aangemaakt", - "domainDeletedDescription": "Domein succesvol verwijderd", - "domainQuestionRemove": "Weet je zeker dat je het domein {domain} uit je account wilt verwijderen?", - "domainMessageRemove": "Na verwijdering zal het domein niet langer aan je account gekoppeld zijn.", - "domainMessageConfirm": "Om te bevestigen, typ hieronder de domeinnaam.", - "domainConfirmDelete": "Bevestig verwijdering van domein", - "domainDelete": "Domein verwijderen", - "domain": "Domein", - "selectDomainTypeNsName": "Domeindelegatie (NS)", - "selectDomainTypeNsDescription": "Dit domein en al zijn subdomeinen. Gebruik dit wanneer je een volledige domeinzone wilt beheersen.", - "selectDomainTypeCnameName": "Enkel domein (CNAME)", - "selectDomainTypeCnameDescription": "Alleen dit specifieke domein. Gebruik dit voor individuele subdomeinen of specifieke domeinvermeldingen.", - "selectDomainTypeWildcardName": "Wildcard Domein", - "selectDomainTypeWildcardDescription": "Dit domein en zijn subdomeinen.", - "domainDelegation": "Enkel domein", - "selectType": "Selecteer een type", - "actions": "acties", - "refresh": "Vernieuwen", - "refreshError": "Het vernieuwen van gegevens is mislukt", - "verified": "Gecontroleerd", - "pending": "In afwachting", - "sidebarBilling": "Facturering", - "billing": "Facturering", - "orgBillingDescription": "Beheer je factureringsgegevens en abonnementen", - "github": "GitHub", - "pangolinHosted": "Pangolin gehost", - "fossorial": "Fossorial", - "completeAccountSetup": "Voltooi accountinstelling", - "completeAccountSetupDescription": "Stel je wachtwoord in om te beginnen", - "accountSetupSent": "We sturen een accountinstellingscode naar dit e-mailadres.", - "accountSetupCode": "Instellingscode", - "accountSetupCodeDescription": "Controleer je e-mail voor de instellingscode.", - "passwordCreate": "Wachtwoord aanmaken", - "passwordCreateConfirm": "Bevestig wachtwoord", - "accountSetupSubmit": "Instellingscode verzenden", - "completeSetup": "Voltooi instellen", - "accountSetupSuccess": "Accountinstelling voltooid! Welkom bij Pangolin!", - "documentation": "Documentatie", - "saveAllSettings": "Alle instellingen opslaan", - "settingsUpdated": "Instellingen bijgewerkt", - "settingsUpdatedDescription": "Alle instellingen zijn succesvol bijgewerkt", - "settingsErrorUpdate": "Bijwerken van instellingen mislukt", - "settingsErrorUpdateDescription": "Er is een fout opgetreden bij het bijwerken van instellingen", - "sidebarCollapse": "Inklappen", - "sidebarExpand": "Uitklappen", - "newtUpdateAvailable": "Update beschikbaar", - "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", - "domainPickerEnterDomain": "Domein", - "domainPickerPlaceholder": "mijnapp.voorbeeld.nl", - "domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.", - "domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien", - "domainPickerTabAll": "Alles", - "domainPickerTabOrganization": "Organisatie", - "domainPickerTabProvided": "Aangeboden", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Beschikbaarheid controleren...", - "domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden. Probeer een ander domein of controleer de domeininstellingen van uw organisatie.", - "domainPickerOrganizationDomains": "Organisatiedomeinen", - "domainPickerProvidedDomains": "Aangeboden domeinen", - "domainPickerSubdomain": "Subdomein: {subdomain}", - "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", - "createNewOrgDescription": "Maak een nieuwe organisatie", - "organization": "Organisatie", - "port": "Poort", - "securityKeyManage": "Beveiligingssleutels beheren", - "securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie", - "securityKeyRegister": "Nieuwe beveiligingssleutel registreren", - "securityKeyList": "Uw beveiligingssleutels", - "securityKeyNone": "Nog geen beveiligingssleutels geregistreerd", - "securityKeyNameRequired": "Naam is verplicht", - "securityKeyRemove": "Verwijderen", - "securityKeyLastUsed": "Laatst gebruikt: {date}", - "securityKeyNameLabel": "Naam", - "securityKeyRegisterSuccess": "Beveiligingssleutel succesvol geregistreerd", - "securityKeyRegisterError": "Fout bij registreren van beveiligingssleutel", - "securityKeyRemoveSuccess": "Beveiligingssleutel succesvol verwijderd", - "securityKeyRemoveError": "Fout bij verwijderen van beveiligingssleutel", - "securityKeyLoadError": "Fout bij laden van beveiligingssleutels", - "securityKeyLogin": "Doorgaan met beveiligingssleutel", - "securityKeyAuthError": "Fout bij authenticatie met beveiligingssleutel", - "securityKeyRecommendation": "Overweeg om een andere beveiligingssleutel te registreren op een ander apparaat om ervoor te zorgen dat u niet buitengesloten raakt van uw account.", - "registering": "Registreren...", - "securityKeyPrompt": "Verifieer je identiteit met je beveiligingssleutel. Zorg ervoor dat je beveiligingssleutel verbonden en klaar is.", - "securityKeyBrowserNotSupported": "Je browser ondersteunt geen beveiligingssleutels. Gebruik een moderne browser zoals Chrome, Firefox of Safari.", - "securityKeyPermissionDenied": "Verleen toegang tot je beveiligingssleutel om door te gaan met inloggen.", - "securityKeyRemovedTooQuickly": "Houd je beveiligingssleutel verbonden totdat het inlogproces is voltooid.", - "securityKeyNotSupported": "Je beveiligingssleutel is mogelijk niet compatibel. Probeer een andere beveiligingssleutel.", - "securityKeyUnknownError": "Er was een probleem met het gebruik van je beveiligingssleutel. Probeer het opnieuw.", - "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.", - "securityKeyAdd": "Beveiligingssleutel toevoegen", - "securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren", - "securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren", - "securityKeyTwoFactorRequired": "Tweestapsverificatie vereist", - "securityKeyTwoFactorDescription": "Voer je tweestapsverificatiecode in om de beveiligingssleutel te registreren", - "securityKeyTwoFactorRemoveDescription": "Voer je tweestapsverificatiecode in om de beveiligingssleutel te verwijderen", - "securityKeyTwoFactorCode": "Tweestapsverificatiecode", - "securityKeyRemoveTitle": "Beveiligingssleutel verwijderen", - "securityKeyRemoveDescription": "Voer je wachtwoord in om de beveiligingssleutel \"{name}\" te verwijderen", - "securityKeyNoKeysRegistered": "Geen beveiligingssleutels geregistreerd", - "securityKeyNoKeysDescription": "Voeg een beveiligingssleutel toe om je accountbeveiliging te verbeteren", - "createDomainRequired": "Domein is vereist", - "createDomainAddDnsRecords": "DNS-records toevoegen", - "createDomainAddDnsRecordsDescription": "Voeg de volgende DNS-records toe aan je domeinprovider om het instellen te voltooien.", - "createDomainNsRecords": "NS-records", - "createDomainRecord": "Record", - "createDomainType": "Type:", - "createDomainName": "Naam:", - "createDomainValue": "Waarde:", - "createDomainCnameRecords": "CNAME-records", - "createDomainARecords": "A Records", - "createDomainRecordNumber": "Record {number}", - "createDomainTxtRecords": "TXT-records", - "createDomainSaveTheseRecords": "Deze records opslaan", - "createDomainSaveTheseRecordsDescription": "Zorg ervoor dat je deze DNS-records opslaat, want je zult ze niet opnieuw zien.", - "createDomainDnsPropagation": "DNS-propagatie", - "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", - "and": "en", - "privacyPolicy": "privacybeleid" - }, - "siteRequired": "Site is vereist.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Gebruik Olm voor clientconnectiviteit", - "errorCreatingClient": "Fout bij het aanmaken van de client", - "clientDefaultsNotFound": "Standaardinstellingen van klant niet gevonden", - "createClient": "Client aanmaken", - "createClientDescription": "Maak een nieuwe client aan om verbinding te maken met uw sites", - "seeAllClients": "Alle clients bekijken", - "clientInformation": "Klantinformatie", - "clientNamePlaceholder": "Clientnaam", - "address": "Adres", - "subnetPlaceholder": "Subnet", - "addressDescription": "Het adres dat deze client zal gebruiken voor connectiviteit", - "selectSites": "Selecteer sites", - "sitesDescription": "De client heeft connectiviteit met de geselecteerde sites", - "clientInstallOlm": "Installeer Olm", - "clientInstallOlmDescription": "Laat Olm draaien op uw systeem", - "clientOlmCredentials": "Olm inloggegevens", - "clientOlmCredentialsDescription": "Dit is hoe Olm zich bij de server zal verifiëren", - "olmEndpoint": "Olm Eindpunt", - "olmId": "Olm ID", - "olmSecretKey": "Olm Geheime Sleutel", - "clientCredentialsSave": "Uw referenties opslaan", - "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.", - "clientUpdateFailed": "Het bijwerken van de client is mislukt", - "clientUpdateError": "Er is een fout opgetreden tijdens het bijwerken van de client.", - "sitesFetchFailed": "Het ophalen van sites is mislukt", - "sitesFetchError": "Er is een fout opgetreden bij het ophalen van sites.", - "olmErrorFetchReleases": "Er is een fout opgetreden bij het ophalen van Olm releases.", - "olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.", - "remoteSubnets": "Externe Subnets", - "enterCidrRange": "Voer CIDR-bereik in", - "remoteSubnetsDescription": "Voeg CIDR-bereiken toe die vanaf deze site op afstand toegankelijk zijn met behulp van clients. Gebruik een formaat zoals 10.0.0.0/24. Dit geldt ALLEEN voor VPN-clientconnectiviteit.", - "resourceEnableProxy": "Openbare proxy inschakelen", - "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", - "externalProxyEnabled": "Externe Proxy Ingeschakeld", - "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", - "domainPickerBaseDomainLabel": "Basisdomein", - "domainPickerSearchDomains": "Zoek domeinen...", - "domainPickerNoDomainsFound": "Geen domeinen gevonden", - "domainPickerLoadingDomains": "Domeinen laden...", - "domainPickerSelectBaseDomain": "Selecteer basisdomein...", - "domainPickerNotAvailableForCname": "Niet beschikbaar voor CNAME-domeinen", - "domainPickerEnterSubdomainOrLeaveBlank": "Voer een subdomein in of laat leeg om basisdomein te gebruiken.", - "domainPickerEnterSubdomainToSearch": "Voer een subdomein in om te zoeken en te selecteren uit beschikbare gratis domeinen.", - "domainPickerFreeDomains": "Gratis Domeinen", - "domainPickerSearchForAvailableDomains": "Zoek naar beschikbare domeinen", - "domainPickerNotWorkSelfHosted": "Opmerking: Gratis aangeboden domeinen zijn momenteel niet beschikbaar voor zelf-gehoste instanties.", - "resourceDomain": "Domein", - "resourceEditDomain": "Domein bewerken", - "siteName": "Site Naam", - "proxyPort": "Poort", - "resourcesTableProxyResources": "Proxybronnen", - "resourcesTableClientResources": "Clientbronnen", - "resourcesTableNoProxyResourcesFound": "Geen proxybronnen gevonden.", - "resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.", - "resourcesTableDestination": "Bestemming", - "resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met", - "resourcesTableClients": "Clienten", - "resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.", - "editInternalResourceDialogEditClientResource": "Bewerk clientbron", - "editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.", - "editInternalResourceDialogResourceProperties": "Bron eigenschappen", - "editInternalResourceDialogName": "Naam", - "editInternalResourceDialogProtocol": "Protocol", - "editInternalResourceDialogSitePort": "Site Poort", - "editInternalResourceDialogTargetConfiguration": "Doelconfiguratie", - "editInternalResourceDialogCancel": "Annuleren", - "editInternalResourceDialogSaveResource": "Sla bron op", - "editInternalResourceDialogSuccess": "Succes", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne bron succesvol bijgewerkt", - "editInternalResourceDialogError": "Fout", - "editInternalResourceDialogFailedToUpdateInternalResource": "Het bijwerken van de interne bron is mislukt", - "editInternalResourceDialogNameRequired": "Naam is verplicht", - "editInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens", - "editInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn", - "editInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn", - "editInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", - "editInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", - "editInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", - "createInternalResourceDialogNoSitesAvailable": "Geen sites beschikbaar", - "createInternalResourceDialogNoSitesAvailableDescription": "U moet ten minste één Newt-site hebben met een geconfigureerd subnet om interne bronnen aan te maken.", - "createInternalResourceDialogClose": "Sluiten", - "createInternalResourceDialogCreateClientResource": "Maak clientbron", - "createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron die toegankelijk zal zijn voor clients die verbonden zijn met de geselecteerde site.", - "createInternalResourceDialogResourceProperties": "Bron-eigenschappen", - "createInternalResourceDialogName": "Naam", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Selecteer site...", - "createInternalResourceDialogSearchSites": "Zoek sites...", - "createInternalResourceDialogNoSitesFound": "Geen sites gevonden.", - "createInternalResourceDialogProtocol": "Protocol", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Poort", - "createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.", - "createInternalResourceDialogTargetConfiguration": "Doelconfiguratie", - "createInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.", - "createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.", - "createInternalResourceDialogCancel": "Annuleren", - "createInternalResourceDialogCreateResource": "Bron aanmaken", - "createInternalResourceDialogSuccess": "Succes", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne bron succesvol aangemaakt", - "createInternalResourceDialogError": "Fout", - "createInternalResourceDialogFailedToCreateInternalResource": "Het aanmaken van de interne bron is mislukt", - "createInternalResourceDialogNameRequired": "Naam is verplicht", - "createInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens", - "createInternalResourceDialogPleaseSelectSite": "Selecteer alstublieft een site", - "createInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn", - "createInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn", - "createInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat", - "createInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn", - "createInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn", - "siteConfiguration": "Configuratie", - "siteAcceptClientConnections": "Accepteer clientverbindingen", - "siteAcceptClientConnectionsDescription": "Sta toe dat andere apparaten verbinding maken via deze Newt-instantie als een gateway met behulp van clients.", - "siteAddress": "Siteadres", - "siteAddressDescription": "Specificeren het IP-adres van de host voor clients om verbinding mee te maken. Dit is het interne adres van de site in het Pangolin netwerk voor clients om te adresseren. Moet binnen het Organisatienetwerk vallen.", - "autoLoginExternalIdp": "Auto Login met Externe IDP", - "autoLoginExternalIdpDescription": "De gebruiker onmiddellijk doorsturen naar de externe IDP voor authenticatie.", - "selectIdp": "Selecteer IDP", - "selectIdpPlaceholder": "Kies een IDP...", - "selectIdpRequired": "Selecteer alstublieft een IDP wanneer automatisch inloggen is ingeschakeld.", - "autoLoginTitle": "Omleiden", - "autoLoginDescription": "Je wordt doorverwezen naar de externe identity provider voor authenticatie.", - "autoLoginProcessing": "Authenticatie voorbereiden...", - "autoLoginRedirecting": "Redirecting naar inloggen...", - "autoLoginError": "Auto Login Fout", - "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", - "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.", - "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", - "introTitle": "Beheerde zelfgehoste pangolin", - "introDescription": "is een implementatieoptie ontworpen voor mensen die eenvoud en extra betrouwbaarheid willen, terwijl hun gegevens privé en zelf georganiseerd blijven.", - "introDetail": "Met deze optie beheert u nog steeds uw eigen Pangolin node - uw tunnels, SSL-verbinding en verkeer alles op uw server. Het verschil is dat beheer en monitoring worden behandeld via onze cloud dashboard, wat een aantal voordelen oplevert:", - "benefitSimplerOperations": { - "title": "Simpler operaties", - "description": "Je hoeft geen eigen mailserver te draaien of complexe waarschuwingen in te stellen. Je krijgt gezondheidscontroles en downtime meldingen uit de box." - }, - "benefitAutomaticUpdates": { - "title": "Automatische updates", - "description": "Het cloud dashboard evolueert snel, zodat u nieuwe functies en bug fixes krijgt zonder elke keer handmatig nieuwe containers te moeten trekken." - }, - "benefitLessMaintenance": { - "title": "Minder onderhoud", - "description": "Geen database migratie, back-ups of extra infrastructuur om te beheren. Dat behandelen we in de cloud." - }, - "benefitCloudFailover": { - "title": "Cloud fout", - "description": "Als uw node omlaag gaat, kunnen uw tunnels tijdelijk niet meer naar onze aanwezigheidspunten gaan totdat u hem weer online brengt." - }, - "benefitHighAvailability": { - "title": "Hoge beschikbaarheid (PoPs)", - "description": "U kunt ook meerdere nodes koppelen aan uw account voor ontslag en betere prestaties." - }, - "benefitFutureEnhancements": { - "title": "Toekomstige verbeteringen", - "description": "We zijn van plan om meer analytica, waarschuwing en beheerhulpmiddelen toe te voegen om uw implementatie nog steviger te maken." - }, - "docsAlert": { - "text": "Meer informatie over de optie voor zelf-verzorging in onze", - "documentation": "documentatie" - }, - "convertButton": "Converteer deze node naar Beheerde Zelf-Hosted" - }, - "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", - "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", - "domainPickerUnverified": "Ongeverifieerd", - "domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.", - "domainPickerError": "Foutmelding", - "domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen", - "domainPickerErrorCheckAvailability": "Kan domein beschikbaarheid niet controleren", - "domainPickerInvalidSubdomain": "Ongeldig subdomein", - "domainPickerInvalidSubdomainRemoved": "De invoer \"{sub}\" is verwijderd omdat het niet geldig is.", - "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", - "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 deleted file mode 100644 index 834fd4be..00000000 --- a/messages/pl-PL.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Utwórz swoją organizację, witrynę i zasoby", - "setupNewOrg": "Nowa organizacja", - "setupCreateOrg": "Utwórz organizację", - "setupCreateResources": "Utwórz Zasoby", - "setupOrgName": "Nazwa organizacji", - "orgDisplayName": "To jest wyświetlana nazwa Twojej organizacji.", - "orgId": "Identyfikator organizacji", - "setupIdentifierMessage": "To jest unikalny identyfikator Twojej organizacji. Jest to oddzielone od nazwy wyświetlanej.", - "setupErrorIdentifier": "Identyfikator organizacji jest już zajęty. Wybierz inny.", - "componentsErrorNoMemberCreate": "Nie jesteś obecnie członkiem żadnej organizacji. Aby rozpocząć, utwórz organizację.", - "componentsErrorNoMember": "Nie jesteś obecnie członkiem żadnej organizacji.", - "welcome": "Witaj w Pangolinie", - "welcomeTo": "Witaj w", - "componentsCreateOrg": "Utwórz organizację", - "componentsMember": "Jesteś członkiem {count, plural, =0 {żadna organizacja} one {jedna organizacja} few {# organizacje} many {# organizacji} other {# organizacji}}.", - "componentsInvalidKey": "Wykryto nieprawidłowe lub wygasłe klucze licencyjne. Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", - "dismiss": "Odrzuć", - "componentsLicenseViolation": "Naruszenie licencji: Ten serwer używa stron {usedSites} , które przekraczają limit licencyjny stron {maxSites} . Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", - "componentsSupporterMessage": "Dziękujemy za wsparcie Pangolina jako {tier}!", - "inviteErrorNotValid": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie zostało zaakceptowane lub jest już nieważne.", - "inviteErrorUser": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie jest dla tego użytkownika.", - "inviteLoginUser": "Upewnij się, że jesteś zalogowany jako właściwy użytkownik.", - "inviteErrorNoUser": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie jest dla użytkownika, który istnieje.", - "inviteCreateUser": "Proszę najpierw utworzyć konto.", - "goHome": "Przejdź do strony głównej", - "inviteLogInOtherUser": "Zaloguj się jako inny użytkownik", - "createAnAccount": "Utwórz konto", - "inviteNotAccepted": "Zaproszenie nie zaakceptowane", - "authCreateAccount": "Utwórz konto, aby rozpocząć", - "authNoAccount": "Nie masz konta?", - "email": "E-mail", - "password": "Hasło", - "confirmPassword": "Potwierdź hasło", - "createAccount": "Utwórz konto", - "viewSettings": "Pokaż ustawienia", - "delete": "Usuń", - "name": "Nazwa", - "online": "Dostępny", - "offline": "Offline", - "site": "Witryna", - "dataIn": "Dane w", - "dataOut": "Dane niedostępne", - "connectionType": "Typ połączenia", - "tunnelType": "Typ tunelu", - "local": "Lokalny", - "edit": "Edytuj", - "siteConfirmDelete": "Potwierdź usunięcie witryny", - "siteDelete": "Usuń witrynę", - "siteMessageRemove": "Po usunięciu, witryna nie będzie już dostępna. Wszystkie zasoby i cele związane z witryną zostaną również usunięte.", - "siteMessageConfirm": "Aby potwierdzić, wpisz nazwę witryny poniżej.", - "siteQuestionRemove": "Czy na pewno chcesz usunąć stronę {selectedSite} z organizacji?", - "siteManageSites": "Zarządzaj stronami", - "siteDescription": "Zezwalaj na połączenie z siecią przez bezpieczne tunele", - "siteCreate": "Utwórz witrynę", - "siteCreateDescription2": "Wykonaj poniższe kroki, aby utworzyć i połączyć nową witrynę", - "siteCreateDescription": "Utwórz nową witrynę, aby rozpocząć łączenie zasobów", - "close": "Zamknij", - "siteErrorCreate": "Błąd podczas tworzenia witryny", - "siteErrorCreateKeyPair": "Nie znaleziono pary kluczy lub domyślnych ustawień witryny", - "siteErrorCreateDefaults": "Nie znaleziono domyślnych ustawień witryny", - "method": "Metoda", - "siteMethodDescription": "W ten sposób ujawnisz połączenia.", - "siteLearnNewt": "Dowiedz się, jak zainstalować Newt w systemie", - "siteSeeConfigOnce": "Możesz zobaczyć konfigurację tylko raz.", - "siteLoadWGConfig": "Ładowanie konfiguracji WireGuard...", - "siteDocker": "Rozwiń o szczegóły wdrożenia dokera", - "toggle": "Przełącz", - "dockerCompose": "Kompozytor dokujący", - "dockerRun": "Uruchom Docker", - "siteLearnLocal": "Lokalne witryny nie tunelowają, dowiedz się więcej", - "siteConfirmCopy": "Skopiowałem konfigurację", - "searchSitesProgress": "Szukaj witryn...", - "siteAdd": "Dodaj witrynę", - "siteInstallNewt": "Zainstaluj Newt", - "siteInstallNewtDescription": "Uruchom Newt w swoim systemie", - "WgConfiguration": "Konfiguracja WireGuard", - "WgConfigurationDescription": "Użyj następującej konfiguracji, aby połączyć się z siecią", - "operatingSystem": "System operacyjny", - "commands": "Polecenia", - "recommended": "Rekomendowane", - "siteNewtDescription": "Aby uzyskać najlepsze doświadczenia użytkownika, użyj Newt. Używa WireGuard pod zapleczem i pozwala na przekierowanie twoich prywatnych zasobów przez ich adres LAN w sieci prywatnej z panelu Pangolin.", - "siteRunsInDocker": "Uruchamia w Docke'u", - "siteRunsInShell": "Uruchamia w skorupce na macOS, Linux i Windows", - "siteErrorDelete": "Błąd podczas usuwania witryny", - "siteErrorUpdate": "Nie udało się zaktualizować witryny", - "siteErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji witryny.", - "siteUpdated": "Strona zaktualizowana", - "siteUpdatedDescription": "Strona została zaktualizowana.", - "siteGeneralDescription": "Skonfiguruj ustawienia ogólne dla tej witryny", - "siteSettingDescription": "Skonfiguruj ustawienia na swojej stronie", - "siteSetting": "Ustawienia {siteName}", - "siteNewtTunnel": "Newt Tunnel (Zalecane)", - "siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.", - "siteWg": "Podstawowy WireGuard", - "siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.", - "siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", - "siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", - "siteSeeAll": "Zobacz wszystkie witryny", - "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", - "siteNewtCredentials": "Aktualne dane logowania", - "siteNewtCredentialsDescription": "Oto jak Newt będzie uwierzytelniał się z serwerem", - "siteCredentialsSave": "Zapisz swoje poświadczenia", - "siteCredentialsSaveDescription": "Możesz to zobaczyć tylko raz. Upewnij się, że skopiuj je do bezpiecznego miejsca.", - "siteInfo": "Informacje o witrynie", - "status": "Status", - "shareTitle": "Zarządzaj linkami udostępniania", - "shareDescription": "Utwórz linki, które można udostępnić, aby przyznać tymczasowy lub stały dostęp do Twoich zasobów", - "shareSearch": "Szukaj linków udostępnienia...", - "shareCreate": "Utwórz link udostępniania", - "shareErrorDelete": "Nie udało się usunąć linku", - "shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku", - "shareDeleted": "Link usunięty", - "shareDeletedDescription": "Link został usunięty", - "shareTokenDescription": "Twój token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.", - "accessToken": "Token dostępu", - "usageExamples": "Przykłady użycia", - "tokenId": "Identyfikator tokena", - "requestHeades": "Nagłówki żądania", - "queryParameter": "Parametr zapytania", - "importantNote": "Ważna uwaga", - "shareImportantDescription": "Ze względów bezpieczeństwa zaleca się użycie nagłówków nad parametrami zapytania, jeśli to możliwe, ponieważ parametry zapytania mogą być zalogowane w dziennikach serwera lub historii przeglądarki.", - "token": "Token", - "shareTokenSecurety": "Chroń swój token dostępu. Nie udostępniaj go w publicznie dostępnych miejscach ani w kodzie po stronie klienta.", - "shareErrorFetchResource": "Nie udało się pobrać zasobów", - "shareErrorFetchResourceDescription": "Wystąpił błąd podczas pobierania zasobów", - "shareErrorCreate": "Nie udało się utworzyć linku udostępniania", - "shareErrorCreateDescription": "Wystąpił błąd podczas tworzenia linku udostępniania", - "shareCreateDescription": "Każdy z tym linkiem może uzyskać dostęp do zasobu", - "shareTitleOptional": "Tytuł (opcjonalnie)", - "expireIn": "Wygasa za", - "neverExpire": "Nigdy nie wygasa", - "shareExpireDescription": "Czas wygaśnięcia to jak długo link będzie mógł być użyty i zapewni dostęp do zasobu. Po tym czasie link nie będzie już działał, a użytkownicy, którzy użyli tego linku, utracą dostęp do zasobu.", - "shareSeeOnce": "Możesz zobaczyć tylko ten link. Upewnij się, że go skopiowało.", - "shareAccessHint": "Każdy z tym linkiem może uzyskać dostęp do zasobu. Podziel się nim ostrożnie.", - "shareTokenUsage": "Zobacz użycie tokenu dostępu", - "createLink": "Utwórz link", - "resourcesNotFound": "Nie znaleziono zasobów", - "resourceSearch": "Szukaj zasobów", - "openMenu": "Otwórz menu", - "resource": "Zasoby", - "title": "Tytuł", - "created": "Utworzono", - "expires": "Wygasa", - "never": "Nigdy", - "shareErrorSelectResource": "Wybierz zasób", - "resourceTitle": "Zarządzaj zasobami", - "resourceDescription": "Utwórz bezpieczne proxy do prywatnych aplikacji", - "resourcesSearch": "Szukaj zasobów...", - "resourceAdd": "Dodaj zasób", - "resourceErrorDelte": "Błąd podczas usuwania zasobu", - "authentication": "Uwierzytelnianie", - "protected": "Chronione", - "notProtected": "Niechronione", - "resourceMessageRemove": "Po usunięciu, zasób nie będzie już dostępny. Wszystkie cele związane z zasobem zostaną również usunięte.", - "resourceMessageConfirm": "Aby potwierdzić, wpisz nazwę zasobu poniżej.", - "resourceQuestionRemove": "Czy na pewno chcesz usunąć zasób {selectedResource} z organizacji?", - "resourceHTTP": "Zasób HTTPS", - "resourceHTTPDescription": "Proxy do Twojej aplikacji przez HTTPS, przy użyciu poddomeny lub domeny bazowej.", - "resourceRaw": "Surowy zasób TCP/UDP", - "resourceRawDescription": "Proxy do aplikacji przez TCP/UDP przy użyciu numeru portu.", - "resourceCreate": "Utwórz zasób", - "resourceCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowy zasób", - "resourceSeeAll": "Zobacz wszystkie zasoby", - "resourceInfo": "Informacje o zasobach", - "resourceNameDescription": "To jest wyświetlana nazwa zasobu.", - "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", - "resourceHTTPSSettings": "Ustawienia HTTPS", - "resourceHTTPSSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez HTTPS", - "domainType": "Typ domeny", - "subdomain": "Poddomena", - "baseDomain": "Bazowa domena", - "subdomnainDescription": "Poddomena, w której twój zasób będzie dostępny.", - "resourceRawSettings": "Ustawienia TCP/UDP", - "resourceRawSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez TCP/UDP", - "protocol": "Protokół", - "protocolSelect": "Wybierz protokół", - "resourcePortNumber": "Numer portu", - "resourcePortNumberDescription": "Numer portu zewnętrznego do żądań proxy.", - "cancel": "Anuluj", - "resourceConfig": "Snippety konfiguracji", - "resourceConfigDescription": "Skopiuj i wklej te fragmenty konfiguracji, aby skonfigurować swój zasób TCP/UDP", - "resourceAddEntrypoints": "Traefik: Dodaj punkty wejścia", - "resourceExposePorts": "Gerbil: Podnieś porty w Komponencie Dockera", - "resourceLearnRaw": "Dowiedz się, jak skonfigurować zasoby TCP/UDP", - "resourceBack": "Powrót do zasobów", - "resourceGoTo": "Przejdź do zasobu", - "resourceDelete": "Usuń zasób", - "resourceDeleteConfirm": "Potwierdź usunięcie zasobu", - "visibility": "Widoczność", - "enabled": "Włączone", - "disabled": "Wyłączone", - "general": "Ogólny", - "generalSettings": "Ustawienia ogólne", - "proxy": "Serwer pośredniczący", - "internal": "Wewętrzny", - "rules": "Regulamin", - "resourceSettingDescription": "Skonfiguruj ustawienia zasobu", - "resourceSetting": "Ustawienia {resourceName}", - "alwaysAllow": "Zawsze zezwalaj", - "alwaysDeny": "Zawsze odmawiaj", - "passToAuth": "Przekaż do Autoryzacji", - "orgSettingsDescription": "Skonfiguruj ustawienia ogólne swojej organizacji", - "orgGeneralSettings": "Ustawienia organizacji", - "orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją", - "saveGeneralSettings": "Zapisz ustawienia ogólne", - "saveSettings": "Zapisz ustawienia", - "orgDangerZone": "Strefa zagrożenia", - "orgDangerZoneDescription": "Po usunięciu tego organa nie ma odwrotu. Upewnij się.", - "orgDelete": "Usuń organizację", - "orgDeleteConfirm": "Potwierdź usunięcie organizacji", - "orgMessageRemove": "Ta akcja jest nieodwracalna i usunie wszystkie powiązane dane.", - "orgMessageConfirm": "Aby potwierdzić, wpisz nazwę organizacji poniżej.", - "orgQuestionRemove": "Czy na pewno chcesz usunąć organizację {selectedOrg}?", - "orgUpdated": "Organizacja zaktualizowana", - "orgUpdatedDescription": "Organizacja została zaktualizowana.", - "orgErrorUpdate": "Nie udało się zaktualizować organizacji", - "orgErrorUpdateMessage": "Wystąpił błąd podczas aktualizacji organizacji.", - "orgErrorFetch": "Nie udało się pobrać organizacji", - "orgErrorFetchMessage": "Wystąpił błąd podczas wyświetlania Twoich organizacji", - "orgErrorDelete": "Nie udało się usunąć organizacji", - "orgErrorDeleteMessage": "Wystąpił błąd podczas usuwania organizacji.", - "orgDeleted": "Organizacja usunięta", - "orgDeletedMessage": "Organizacja i jej dane zostały usunięte.", - "orgMissing": "Brak ID organizacji", - "orgMissingMessage": "Nie można ponownie wygenerować zaproszenia bez ID organizacji.", - "accessUsersManage": "Zarządzaj użytkownikami", - "accessUsersDescription": "Zaproś użytkowników i dodaj je do ról do zarządzania dostępem do Twojej organizacji", - "accessUsersSearch": "Szukaj użytkowników...", - "accessUserCreate": "Utwórz użytkownika", - "accessUserRemove": "Usuń użytkownika", - "username": "Nazwa użytkownika", - "identityProvider": "Dostawca tożsamości", - "role": "Rola", - "nameRequired": "Nazwa jest wymagana", - "accessRolesManage": "Zarządzaj rolami", - "accessRolesDescription": "Skonfiguruj role do zarządzania dostępem do Twojej organizacji", - "accessRolesSearch": "Szukaj ról...", - "accessRolesAdd": "Dodaj rolę", - "accessRoleDelete": "Usuń rolę", - "description": "Opis", - "inviteTitle": "Otwórz zaproszenia", - "inviteDescription": "Zarządzaj zaproszeniami dla innych użytkowników", - "inviteSearch": "Szukaj zaproszeń...", - "minutes": "Protokoły", - "hours": "Godziny", - "days": "Dni", - "weeks": "Tygodnie", - "months": "Miesiące", - "years": "Lata", - "day": "{count, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}", - "apiKeysTitle": "Informacje o kluczu API", - "apiKeysConfirmCopy2": "Musisz potwierdzić, że skopiowałeś klucz API.", - "apiKeysErrorCreate": "Błąd podczas tworzenia klucza API", - "apiKeysErrorSetPermission": "Błąd podczas ustawiania uprawnień", - "apiKeysCreate": "Generuj klucz API", - "apiKeysCreateDescription": "Wygeneruj nowy klucz API dla swojej organizacji", - "apiKeysGeneralSettings": "Uprawnienia", - "apiKeysGeneralSettingsDescription": "Określ, co ten klucz API może zrobić", - "apiKeysList": "Twój klucz API", - "apiKeysSave": "Zapisz swój klucz API", - "apiKeysSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.", - "apiKeysInfo": "Twój klucz API to:", - "apiKeysConfirmCopy": "Skopiowałem klucz API", - "generate": "Generuj", - "done": "Gotowe", - "apiKeysSeeAll": "Zobacz wszystkie klucze API", - "apiKeysPermissionsErrorLoadingActions": "Błąd podczas ładowania akcji klucza API", - "apiKeysPermissionsErrorUpdate": "Błąd podczas ustawiania uprawnień", - "apiKeysPermissionsUpdated": "Uprawnienia zaktualizowane", - "apiKeysPermissionsUpdatedDescription": "Uprawnienia zostały zaktualizowane.", - "apiKeysPermissionsGeneralSettings": "Uprawnienia", - "apiKeysPermissionsGeneralSettingsDescription": "Określ, co ten klucz API może zrobić", - "apiKeysPermissionsSave": "Zapisz uprawnienia", - "apiKeysPermissionsTitle": "Uprawnienia", - "apiKeys": "Klucze API", - "searchApiKeys": "Szukaj kluczy API...", - "apiKeysAdd": "Generuj klucz API", - "apiKeysErrorDelete": "Błąd podczas usuwania klucza API", - "apiKeysErrorDeleteMessage": "Błąd podczas usuwania klucza API", - "apiKeysQuestionRemove": "Czy na pewno chcesz usunąć klucz API {selectedApiKey} z organizacji?", - "apiKeysMessageRemove": "Po usunięciu klucz API nie będzie już mógł być używany.", - "apiKeysMessageConfirm": "Aby potwierdzić, wpisz nazwę klucza API poniżej.", - "apiKeysDeleteConfirm": "Potwierdź usunięcie klucza API", - "apiKeysDelete": "Usuń klucz API", - "apiKeysManage": "Zarządzaj kluczami API", - "apiKeysDescription": "Klucze API służą do uwierzytelniania z API integracji", - "apiKeysSettings": "Ustawienia {apiKeyName}", - "userTitle": "Zarządzaj wszystkimi użytkownikami", - "userDescription": "Zobacz i zarządzaj wszystkimi użytkownikami w systemie", - "userAbount": "O zarządzaniu użytkownikami", - "userAbountDescription": "Ta tabela wyświetla wszystkie obiekty użytkownika root w systemie. Każdy użytkownik może należeć do wielu organizacji. Usunięcie użytkownika z organizacji nie usuwa ich głównego obiektu użytkownika - pozostanie on w systemie. Aby całkowicie usunąć użytkownika z systemu, musisz usunąć jego obiekt root użytkownika za pomocą akcji usuwania z tej tabeli.", - "userServer": "Użytkownicy serwera", - "userSearch": "Szukaj użytkowników serwera...", - "userErrorDelete": "Błąd podczas usuwania użytkownika", - "userDeleteConfirm": "Potwierdź usunięcie użytkownika", - "userDeleteServer": "Usuń użytkownika z serwera", - "userMessageRemove": "Użytkownik zostanie usunięty ze wszystkich organizacji i całkowicie usunięty z serwera.", - "userMessageConfirm": "Aby potwierdzić, wpisz nazwę użytkownika poniżej.", - "userQuestionRemove": "Czy na pewno chcesz trwale usunąć {selectedUser} z serwera?", - "licenseKey": "Klucz licencyjny", - "valid": "Prawidłowy", - "numberOfSites": "Liczba witryn", - "licenseKeySearch": "Szukaj kluczy licencyjnych...", - "licenseKeyAdd": "Dodaj klucz licencyjny", - "type": "Typ", - "licenseKeyRequired": "Klucz licencyjny jest wymagany", - "licenseTermsAgree": "Musisz wyrazić zgodę na warunki licencji", - "licenseErrorKeyLoad": "Nie udało się załadować kluczy licencyjnych", - "licenseErrorKeyLoadDescription": "Wystąpił błąd podczas ładowania kluczy licencyjnych.", - "licenseErrorKeyDelete": "Nie udało się usunąć klucza licencyjnego", - "licenseErrorKeyDeleteDescription": "Wystąpił błąd podczas usuwania klucza licencyjnego.", - "licenseKeyDeleted": "Klucz licencji został usunięty", - "licenseKeyDeletedDescription": "Klucz licencyjny został usunięty.", - "licenseErrorKeyActivate": "Nie udało się aktywować klucza licencji", - "licenseErrorKeyActivateDescription": "Wystąpił błąd podczas aktywacji klucza licencyjnego.", - "licenseAbout": "O licencjonowaniu", - "communityEdition": "Edycja Społeczności", - "licenseAboutDescription": "Dotyczy to przedsiębiorstw i przedsiębiorstw, którzy stosują Pangolin w środowisku handlowym. Jeśli używasz Pangolin do użytku osobistego, możesz zignorować tę sekcję.", - "licenseKeyActivated": "Klucz licencyjny aktywowany", - "licenseKeyActivatedDescription": "Klucz licencyjny został pomyślnie aktywowany.", - "licenseErrorKeyRecheck": "Nie udało się ponownie sprawdzić kluczy licencyjnych", - "licenseErrorKeyRecheckDescription": "Wystąpił błąd podczas ponownego sprawdzania kluczy licencyjnych.", - "licenseErrorKeyRechecked": "Klucze licencyjne ponownie sprawdzone", - "licenseErrorKeyRecheckedDescription": "Wszystkie klucze licencyjne zostały ponownie sprawdzone", - "licenseActivateKey": "Aktywuj klucz licencyjny", - "licenseActivateKeyDescription": "Wprowadź klucz licencyjny, aby go aktywować.", - "licenseActivate": "Aktywuj licencję", - "licenseAgreement": "Zaznaczając to pole, potwierdzasz, że przeczytałeś i zgadzasz się na warunki licencji odpowiadające poziomowi powiązanemu z kluczem licencyjnym.", - "fossorialLicense": "Zobacz Fossorial Commercial License & Subskrypcja", - "licenseMessageRemove": "Spowoduje to usunięcie klucza licencyjnego i wszystkich przypisanych przez niego uprawnień.", - "licenseMessageConfirm": "Aby potwierdzić, wpisz klucz licencyjny poniżej.", - "licenseQuestionRemove": "Czy na pewno chcesz usunąć klucz licencyjny {selectedKey}?", - "licenseKeyDelete": "Usuń klucz licencyjny", - "licenseKeyDeleteConfirm": "Potwierdź usunięcie klucza licencyjnego", - "licenseTitle": "Zarządzaj statusem licencji", - "licenseTitleDescription": "Wyświetl i zarządzaj kluczami licencyjnymi w systemie", - "licenseHost": "Licencja hosta", - "licenseHostDescription": "Zarządzaj głównym kluczem licencyjnym hosta.", - "licensedNot": "Brak licencji", - "hostId": "ID hosta", - "licenseReckeckAll": "Sprawdź ponownie wszystkie klucze", - "licenseSiteUsage": "Użycie witryn", - "licenseSiteUsageDecsription": "Zobacz liczbę witryn korzystających z tej licencji.", - "licenseNoSiteLimit": "Nie ma limitu liczby witryn używających nielicencjonowanego hosta.", - "licensePurchase": "Kup licencję", - "licensePurchaseSites": "Kup dodatkowe witryny", - "licenseSitesUsedMax": "Użyte strony {usedSites} z {maxSites}", - "licenseSitesUsed": "{count, plural, =0 {# witryn} one {# witryna} few {# witryny} many {# witryn} other {# witryn}} w systemie.", - "licensePurchaseDescription": "Wybierz ile witryn chcesz {selectedMode, select, license {kupić licencję. Zawsze możesz dodać więcej witryn później.} other {dodaj do swojej istniejącej licencji.}}", - "licenseFee": "Opłata licencyjna", - "licensePriceSite": "Cena za witrynę", - "total": "Łącznie", - "licenseContinuePayment": "Przejdź do płatności", - "pricingPage": "strona cenowa", - "pricingPortal": "Zobacz portal zakupu", - "licensePricingPage": "Aby uzyskać najnowsze ceny i rabaty, odwiedź ", - "invite": "Zaproszenia", - "inviteRegenerate": "Wygeneruj ponownie zaproszenie", - "inviteRegenerateDescription": "Unieważnij poprzednie zaproszenie i utwórz nowe", - "inviteRemove": "Usuń zaproszenie", - "inviteRemoveError": "Nie udało się usunąć zaproszenia", - "inviteRemoveErrorDescription": "Wystąpił błąd podczas usuwania zaproszenia.", - "inviteRemoved": "Zaproszenie usunięte", - "inviteRemovedDescription": "Zaproszenie dla {email} zostało usunięte.", - "inviteQuestionRemove": "Czy na pewno chcesz usunąć zaproszenie {email}?", - "inviteMessageRemove": "Po usunięciu to zaproszenie nie będzie już ważne. Zawsze możesz ponownie zaprosić użytkownika później.", - "inviteMessageConfirm": "Aby potwierdzić, wpisz poniżej adres email zaproszenia.", - "inviteQuestionRegenerate": "Czy na pewno chcesz ponownie wygenerować zaproszenie {email}? Spowoduje to unieważnienie poprzedniego zaproszenia.", - "inviteRemoveConfirm": "Potwierdź usunięcie zaproszenia", - "inviteRegenerated": "Zaproszenie wygenerowane ponownie", - "inviteSent": "Nowe zaproszenie zostało wysłane do {email}.", - "inviteSentEmail": "Wyślij powiadomienie email do użytkownika", - "inviteGenerate": "Nowe zaproszenie zostało wygenerowane dla {email}.", - "inviteDuplicateError": "Zduplikowane zaproszenie", - "inviteDuplicateErrorDescription": "Zaproszenie dla tego użytkownika już istnieje.", - "inviteRateLimitError": "Przekroczono limit żądań", - "inviteRateLimitErrorDescription": "Przekroczyłeś limit 3 regeneracji na godzinę. Spróbuj ponownie później.", - "inviteRegenerateError": "Nie udało się ponownie wygenerować zaproszenia", - "inviteRegenerateErrorDescription": "Wystąpił błąd podczas ponownego generowania zaproszenia.", - "inviteValidityPeriod": "Okres ważności", - "inviteValidityPeriodSelect": "Wybierz okres ważności", - "inviteRegenerateMessage": "Zaproszenie zostało ponownie wygenerowane. Użytkownik musi uzyskać dostęp do poniższego linku, aby zaakceptować zaproszenie.", - "inviteRegenerateButton": "Wygeneruj ponownie", - "expiresAt": "Wygasa w dniu", - "accessRoleUnknown": "Nieznana rola", - "placeholder": "Symbol zastępczy", - "userErrorOrgRemove": "Nie udało się usunąć użytkownika", - "userErrorOrgRemoveDescription": "Wystąpił błąd podczas usuwania użytkownika.", - "userOrgRemoved": "Użytkownik usunięty", - "userOrgRemovedDescription": "Użytkownik {email} został usunięty z organizacji.", - "userQuestionOrgRemove": "Czy na pewno chcesz usunąć {email} z organizacji?", - "userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.", - "userMessageOrgConfirm": "Aby potwierdzić, wpisz nazwę użytkownika poniżej.", - "userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika", - "userRemoveOrg": "Usuń użytkownika z organizacji", - "users": "Użytkownicy", - "accessRoleMember": "Członek", - "accessRoleOwner": "Właściciel", - "userConfirmed": "Potwierdzony", - "idpNameInternal": "Wewnętrzny", - "emailInvalid": "Nieprawidłowy adres e-mail", - "inviteValidityDuration": "Proszę wybrać okres ważności", - "accessRoleSelectPlease": "Proszę wybrać rolę", - "usernameRequired": "Nazwa użytkownika jest wymagana", - "idpSelectPlease": "Proszę wybrać dostawcę tożsamości", - "idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.", - "accessRoleErrorFetch": "Nie udało się pobrać ról", - "accessRoleErrorFetchDescription": "Wystąpił błąd podczas pobierania ról", - "idpErrorFetch": "Nie udało się pobrać dostawców tożsamości", - "idpErrorFetchDescription": "Wystąpił błąd podczas pobierania dostawców tożsamości", - "userErrorExists": "Użytkownik już istnieje", - "userErrorExistsDescription": "Ten użytkownik jest już członkiem organizacji.", - "inviteError": "Nie udało się zaprosić użytkownika", - "inviteErrorDescription": "Wystąpił błąd podczas zapraszania użytkownika", - "userInvited": "Użytkownik zaproszony", - "userInvitedDescription": "Użytkownik został pomyślnie zaproszony.", - "userErrorCreate": "Nie udało się utworzyć użytkownika", - "userErrorCreateDescription": "Wystąpił błąd podczas tworzenia użytkownika", - "userCreated": "Utworzono użytkownika", - "userCreatedDescription": "Użytkownik został pomyślnie utworzony.", - "userTypeInternal": "Użytkownik wewnętrzny", - "userTypeInternalDescription": "Zaproś użytkownika do bezpośredniego dołączenia do Twojej organizacji.", - "userTypeExternal": "Użytkownik zewnętrzny", - "userTypeExternalDescription": "Utwórz użytkownika z zewnętrznym dostawcą tożsamości.", - "accessUserCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowego użytkownika", - "userSeeAll": "Zobacz wszystkich użytkowników", - "userTypeTitle": "Typ użytkownika", - "userTypeDescription": "Określ, jak chcesz utworzyć użytkownika", - "userSettings": "Informacje o użytkowniku", - "userSettingsDescription": "Wprowadź dane nowego użytkownika", - "inviteEmailSent": "Wyślij email z zaproszeniem do użytkownika", - "inviteValid": "Ważne przez", - "selectDuration": "Wybierz okres", - "accessRoleSelect": "Wybierz rolę", - "inviteEmailSentDescription": "Email został wysłany do użytkownika z linkiem dostępu poniżej. Musi on uzyskać dostęp do linku, aby zaakceptować zaproszenie.", - "inviteSentDescription": "Użytkownik został zaproszony. Musi uzyskać dostęp do poniższego linku, aby zaakceptować zaproszenie.", - "inviteExpiresIn": "Zaproszenie wygaśnie za {days, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}.", - "idpTitle": "Informacje ogólne", - "idpSelect": "Wybierz dostawcę tożsamości dla użytkownika zewnętrznego", - "idpNotConfigured": "Nie skonfigurowano żadnych dostawców tożsamości. Skonfiguruj dostawcę tożsamości przed utworzeniem użytkowników zewnętrznych.", - "usernameUniq": "Musi to odpowiadać unikalnej nazwie użytkownika istniejącej u wybranego dostawcy tożsamości.", - "emailOptional": "Email (Opcjonalnie)", - "nameOptional": "Nazwa (Opcjonalnie)", - "accessControls": "Kontrola dostępu", - "userDescription2": "Zarządzaj ustawieniami tego użytkownika", - "accessRoleErrorAdd": "Nie udało się dodać użytkownika do roli", - "accessRoleErrorAddDescription": "Wystąpił błąd podczas dodawania użytkownika do roli.", - "userSaved": "Użytkownik zapisany", - "userSavedDescription": "Użytkownik został zaktualizowany.", - "autoProvisioned": "Przesłane automatycznie", - "autoProvisionedDescription": "Pozwól temu użytkownikowi na automatyczne zarządzanie przez dostawcę tożsamości", - "accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji", - "accessControlsSubmit": "Zapisz kontrole dostępu", - "roles": "Role", - "accessUsersRoles": "Zarządzaj użytkownikami i rolami", - "accessUsersRolesDescription": "Zapraszaj użytkowników i dodawaj ich do ról, aby zarządzać dostępem do Twojej organizacji", - "key": "Klucz", - "createdAt": "Utworzono", - "proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.", - "proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.", - "proxyEnableSSL": "Włącz SSL", - "proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.", - "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", - "siteErrorFetchDescription": "Wystąpił błąd podczas pobierania zasobu", - "targetErrorDuplicate": "Duplikat celu", - "targetErrorDuplicateDescription": "Cel o tych ustawieniach już istnieje", - "targetWireGuardErrorInvalidIp": "Nieprawidłowy adres IP celu", - "targetWireGuardErrorInvalidIpDescription": "Adres IP celu musi znajdować się w podsieci witryny", - "targetsUpdated": "Cele zaktualizowane", - "targetsUpdatedDescription": "Cele i ustawienia zostały pomyślnie zaktualizowane", - "targetsErrorUpdate": "Nie udało się zaktualizować celów", - "targetsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji celów", - "targetTlsUpdate": "Ustawienia TLS zaktualizowane", - "targetTlsUpdateDescription": "Twoje ustawienia TLS zostały pomyślnie zaktualizowane", - "targetErrorTlsUpdate": "Nie udało się zaktualizować ustawień TLS", - "targetErrorTlsUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień TLS", - "proxyUpdated": "Ustawienia proxy zaktualizowane", - "proxyUpdatedDescription": "Twoje ustawienia proxy zostały pomyślnie zaktualizowane", - "proxyErrorUpdate": "Nie udało się zaktualizować ustawień proxy", - "proxyErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień proxy", - "targetAddr": "IP / Nazwa hosta", - "targetPort": "Port", - "targetProtocol": "Protokół", - "targetTlsSettings": "Konfiguracja bezpiecznego połączenia", - "targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu", - "targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS", - "targetTlsSni": "Nazwa serwera TLS", - "targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.", - "targetTlsSubmit": "Zapisz ustawienia", - "targets": "Konfiguracja celów", - "targetsDescription": "Skonfiguruj cele do kierowania ruchu do usług zaplecza", - "targetStickySessions": "Włącz sesje trwałe", - "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", - "methodSelect": "Wybierz metodę", - "targetSubmit": "Dodaj cel", - "targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.", - "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", - "proxyCustomHeaderDescription": "Nagłówek hosta do ustawienia podczas proxy żądań. Pozostaw puste, aby użyć domyślnego.", - "proxyAdditionalSubmit": "Zapisz ustawienia proxy", - "subnetMaskErrorInvalid": "Nieprawidłowa maska podsieci. Musi być między 0 a 32.", - "ipAddressErrorInvalidFormat": "Nieprawidłowy format adresu IP", - "ipAddressErrorInvalidOctet": "Nieprawidłowy oktet adresu IP", - "path": "Ścieżka", - "matchPath": "Ścieżka dopasowania", - "ipAddressRange": "Zakres IP", - "rulesErrorFetch": "Nie udało się pobrać reguł", - "rulesErrorFetchDescription": "Wystąpił błąd podczas pobierania reguł", - "rulesErrorDuplicate": "Duplikat reguły", - "rulesErrorDuplicateDescription": "Reguła o tych ustawieniach już istnieje", - "rulesErrorInvalidIpAddressRange": "Nieprawidłowy CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Wprowadź prawidłową wartość CIDR", - "rulesErrorInvalidUrl": "Nieprawidłowa ścieżka URL", - "rulesErrorInvalidUrlDescription": "Wprowadź prawidłową wartość ścieżki URL", - "rulesErrorInvalidIpAddress": "Nieprawidłowe IP", - "rulesErrorInvalidIpAddressDescription": "Wprowadź prawidłowy adres IP", - "rulesErrorUpdate": "Nie udało się zaktualizować reguł", - "rulesErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji reguł", - "rulesUpdated": "Włącz reguły", - "rulesUpdatedDescription": "Ocena reguł została zaktualizowana", - "rulesMatchIpAddressRangeDescription": "Wprowadź adres w formacie CIDR (np. 103.21.244.0/22)", - "rulesMatchIpAddress": "Wprowadź adres IP (np. 103.21.244.12)", - "rulesMatchUrl": "Wprowadź ścieżkę URL lub wzorzec (np. /api/v1/todos lub /api/v1/*)", - "rulesErrorInvalidPriority": "Nieprawidłowy priorytet", - "rulesErrorInvalidPriorityDescription": "Wprowadź prawidłowy priorytet", - "rulesErrorDuplicatePriority": "Zduplikowane priorytety", - "rulesErrorDuplicatePriorityDescription": "Wprowadź unikalne priorytety", - "ruleUpdated": "Reguły zaktualizowane", - "ruleUpdatedDescription": "Reguły zostały pomyślnie zaktualizowane", - "ruleErrorUpdate": "Operacja nie powiodła się", - "ruleErrorUpdateDescription": "Wystąpił błąd podczas operacji zapisu", - "rulesPriority": "Priorytet", - "rulesAction": "Akcja", - "rulesMatchType": "Typ dopasowania", - "value": "Wartość", - "rulesAbout": "O regułach", - "rulesAboutDescription": "Reguły pozwalają kontrolować dostęp do zasobu na podstawie zestawu kryteriów. Możesz tworzyć reguły zezwalające lub odmawiające dostępu na podstawie adresu IP lub ścieżki URL.", - "rulesActions": "Akcje", - "rulesActionAlwaysAllow": "Zawsze zezwalaj: Pomiń wszystkie metody uwierzytelniania", - "rulesActionAlwaysDeny": "Zawsze odmawiaj: Blokuj wszystkie żądania; nie można próbować uwierzytelniania", - "rulesActionPassToAuth": "Przekaż do Autoryzacji: Zezwól na próby metod uwierzytelniania", - "rulesMatchCriteria": "Kryteria dopasowania", - "rulesMatchCriteriaIpAddress": "Dopasuj konkretny adres IP", - "rulesMatchCriteriaIpAddressRange": "Dopasuj zakres adresów IP w notacji CIDR", - "rulesMatchCriteriaUrl": "Dopasuj ścieżkę URL lub wzorzec", - "rulesEnable": "Włącz reguły", - "rulesEnableDescription": "Włącz lub wyłącz ocenę reguł dla tego zasobu", - "rulesResource": "Konfiguracja reguł zasobu", - "rulesResourceDescription": "Skonfiguruj reguły kontroli dostępu do zasobu", - "ruleSubmit": "Dodaj regułę", - "rulesNoOne": "Brak reguł. Dodaj regułę używając formularza.", - "rulesOrder": "Reguły są oceniane według priorytetu w kolejności rosnącej.", - "rulesSubmit": "Zapisz reguły", - "resourceErrorCreate": "Błąd podczas tworzenia zasobu", - "resourceErrorCreateDescription": "Wystąpił błąd podczas tworzenia zasobu", - "resourceErrorCreateMessage": "Błąd podczas tworzenia zasobu:", - "resourceErrorCreateMessageDescription": "Wystąpił nieoczekiwany błąd", - "sitesErrorFetch": "Błąd podczas pobierania witryn", - "sitesErrorFetchDescription": "Wystąpił błąd podczas pobierania witryn", - "domainsErrorFetch": "Błąd podczas pobierania domen", - "domainsErrorFetchDescription": "Wystąpił błąd podczas pobierania domen", - "none": "Brak", - "unknown": "Nieznany", - "resources": "Zasoby", - "resourcesDescription": "Zasoby są proxy do aplikacji działających w Twojej sieci prywatnej. Utwórz zasób dla dowolnej usługi HTTP/HTTPS lub surowej TCP/UDP w Twojej sieci prywatnej. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne, bezpieczne połączenie przez zaszyfrowany tunel WireGuard.", - "resourcesWireGuardConnect": "Bezpieczne połączenie z szyfrowaniem WireGuard", - "resourcesMultipleAuthenticationMethods": "Skonfiguruj wiele metod uwierzytelniania", - "resourcesUsersRolesAccess": "Kontrola dostępu oparta na użytkownikach i rolach", - "resourcesErrorUpdate": "Nie udało się przełączyć zasobu", - "resourcesErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji zasobu", - "access": "Dostęp", - "shareLink": "Link udostępniania {resource}", - "resourceSelect": "Wybierz zasób", - "shareLinks": "Linki udostępniania", - "share": "Linki do udostępniania", - "shareDescription2": "Twórz linki do udostępniania swoich zasobów. Linki zapewniają tymczasowy lub nieograniczony dostęp do zasobu. Podczas tworzenia linku możesz skonfigurować okres jego ważności.", - "shareEasyCreate": "Łatwe tworzenie i udostępnianie", - "shareConfigurableExpirationDuration": "Konfigurowalny okres ważności", - "shareSecureAndRevocable": "Bezpieczne i odwoływalne", - "nameMin": "Nazwa musi mieć co najmniej {len} znaków.", - "nameMax": "Nazwa nie może być dłuższa niż {len} znaków.", - "sitesConfirmCopy": "Potwierdź, że skopiowałeś konfigurację.", - "unknownCommand": "Nieznane polecenie", - "newtErrorFetchReleases": "Nie udało się pobrać informacji o wydaniu: {err}", - "newtErrorFetchLatest": "Błąd podczas pobierania najnowszego wydania: {err}", - "newtEndpoint": "Punkt końcowy Newt", - "newtId": "ID Newt", - "newtSecretKey": "Klucz tajny Newt", - "architecture": "Architektura", - "sites": "Witryny", - "siteWgAnyClients": "Użyj dowolnego klienta WireGuard do połączenia. Będziesz musiał adresować swoje zasoby wewnętrzne używając IP peera.", - "siteWgCompatibleAllClients": "Kompatybilny ze wszystkimi klientami WireGuard", - "siteWgManualConfigurationRequired": "Wymagana konfiguracja ręczna", - "userErrorNotAdminOrOwner": "Użytkownik nie jest administratorem ani właścicielem", - "pangolinSettings": "Ustawienia - Pangolin", - "accessRoleYour": "Twoja rola:", - "accessRoleSelect2": "Wybierz rolę", - "accessUserSelect": "Wybierz użytkownika", - "otpEmailEnter": "Wprowadź adres e-mail", - "otpEmailEnterDescription": "Naciśnij enter, aby dodać adres e-mail po wpisaniu go w polu.", - "otpEmailErrorInvalid": "Nieprawidłowy adres e-mail. Znak wieloznaczny (*) musi być całą częścią lokalną.", - "otpEmailSmtpRequired": "Wymagany SMTP", - "otpEmailSmtpRequiredDescription": "SMTP musi być włączony na serwerze, aby korzystać z uwierzytelniania jednorazowym hasłem.", - "otpEmailTitle": "Hasła jednorazowe", - "otpEmailTitleDescription": "Wymagaj uwierzytelniania opartego na e-mail dla dostępu do zasobu", - "otpEmailWhitelist": "Biała lista e-mail", - "otpEmailWhitelistList": "Dozwolone adresy e-mail", - "otpEmailWhitelistListDescription": "Tylko użytkownicy z tymi adresami e-mail będą mieli dostęp do tego zasobu. Otrzymają prośbę o wprowadzenie jednorazowego hasła wysłanego na ich e-mail. Można użyć znaków wieloznacznych (*@example.com), aby zezwolić na dowolny adres e-mail z domeny.", - "otpEmailWhitelistSave": "Zapisz białą listę", - "passwordAdd": "Dodaj hasło", - "passwordRemove": "Usuń hasło", - "pincodeAdd": "Dodaj kod PIN", - "pincodeRemove": "Usuń kod PIN", - "resourceAuthMethods": "Metody uwierzytelniania", - "resourceAuthMethodsDescriptions": "Zezwól na dostęp do zasobu przez dodatkowe metody uwierzytelniania", - "resourceAuthSettingsSave": "Zapisano pomyślnie", - "resourceAuthSettingsSaveDescription": "Ustawienia uwierzytelniania zostały zapisane", - "resourceErrorAuthFetch": "Nie udało się pobrać danych", - "resourceErrorAuthFetchDescription": "Wystąpił błąd podczas pobierania danych", - "resourceErrorPasswordRemove": "Błąd podczas usuwania hasła zasobu", - "resourceErrorPasswordRemoveDescription": "Wystąpił błąd podczas usuwania hasła zasobu", - "resourceErrorPasswordSetup": "Błąd podczas ustawiania hasła zasobu", - "resourceErrorPasswordSetupDescription": "Wystąpił błąd podczas ustawiania hasła zasobu", - "resourceErrorPincodeRemove": "Błąd podczas usuwania kodu PIN zasobu", - "resourceErrorPincodeRemoveDescription": "Wystąpił błąd podczas usuwania kodu PIN zasobu", - "resourceErrorPincodeSetup": "Błąd podczas ustawiania kodu PIN zasobu", - "resourceErrorPincodeSetupDescription": "Wystąpił błąd podczas ustawiania kodu PIN zasobu", - "resourceErrorUsersRolesSave": "Nie udało się ustawić ról", - "resourceErrorUsersRolesSaveDescription": "Wystąpił błąd podczas ustawiania ról", - "resourceErrorWhitelistSave": "Nie udało się zapisać białej listy", - "resourceErrorWhitelistSaveDescription": "Wystąpił błąd podczas zapisywania białej listy", - "resourcePasswordSubmit": "Włącz ochronę hasłem", - "resourcePasswordProtection": "Ochrona haseł {status}", - "resourcePasswordRemove": "Hasło zasobu zostało usunięte", - "resourcePasswordRemoveDescription": "Hasło zasobu zostało pomyślnie usunięte", - "resourcePasswordSetup": "Ustawiono hasło zasobu", - "resourcePasswordSetupDescription": "Hasło zasobu zostało pomyślnie ustawione", - "resourcePasswordSetupTitle": "Ustaw hasło", - "resourcePasswordSetupTitleDescription": "Ustaw hasło, aby chronić ten zasób", - "resourcePincode": "Kod PIN", - "resourcePincodeSubmit": "Włącz ochronę kodem PIN", - "resourcePincodeProtection": "Ochrona kodem PIN {status}", - "resourcePincodeRemove": "Usunięto kod PIN zasobu", - "resourcePincodeRemoveDescription": "Kod PIN zasobu został pomyślnie usunięty", - "resourcePincodeSetup": "Ustawiono kod PIN zasobu", - "resourcePincodeSetupDescription": "Kod PIN zasobu został pomyślnie ustawiony", - "resourcePincodeSetupTitle": "Ustaw kod PIN", - "resourcePincodeSetupTitleDescription": "Ustaw kod PIN, aby chronić ten zasób", - "resourceRoleDescription": "Administratorzy zawsze mają dostęp do tego zasobu.", - "resourceUsersRoles": "Użytkownicy i role", - "resourceUsersRolesDescription": "Skonfiguruj, którzy użytkownicy i role mogą odwiedzać ten zasób", - "resourceUsersRolesSubmit": "Zapisz użytkowników i role", - "resourceWhitelistSave": "Zapisano pomyślnie", - "resourceWhitelistSaveDescription": "Ustawienia białej listy zostały zapisane", - "ssoUse": "Użyj platformy SSO", - "ssoUseDescription": "Istniejący użytkownicy będą musieli zalogować się tylko raz dla wszystkich zasobów, które mają to włączone.", - "proxyErrorInvalidPort": "Nieprawidłowy numer portu", - "subdomainErrorInvalid": "Nieprawidłowa poddomena", - "domainErrorFetch": "Błąd podczas pobierania domen", - "domainErrorFetchDescription": "Wystąpił błąd podczas pobierania domen", - "resourceErrorUpdate": "Nie udało się zaktualizować zasobu", - "resourceErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji zasobu", - "resourceUpdated": "Zasób zaktualizowany", - "resourceUpdatedDescription": "Zasób został pomyślnie zaktualizowany", - "resourceErrorTransfer": "Nie udało się przenieść zasobu", - "resourceErrorTransferDescription": "Wystąpił błąd podczas przenoszenia zasobu", - "resourceTransferred": "Zasób przeniesiony", - "resourceTransferredDescription": "Zasób został pomyślnie przeniesiony", - "resourceErrorToggle": "Nie udało się przełączyć zasobu", - "resourceErrorToggleDescription": "Wystąpił błąd podczas aktualizacji zasobu", - "resourceVisibilityTitle": "Widoczność", - "resourceVisibilityTitleDescription": "Całkowicie włącz lub wyłącz widoczność zasobu", - "resourceGeneral": "Ustawienia ogólne", - "resourceGeneralDescription": "Skonfiguruj ustawienia ogólne dla tego zasobu", - "resourceEnable": "Włącz zasób", - "resourceTransfer": "Przenieś zasób", - "resourceTransferDescription": "Przenieś ten zasób do innej witryny", - "resourceTransferSubmit": "Przenieś zasób", - "siteDestination": "Witryna docelowa", - "searchSites": "Szukaj witryn", - "accessRoleCreate": "Utwórz rolę", - "accessRoleCreateDescription": "Utwórz nową rolę aby zgrupować użytkowników i zarządzać ich uprawnieniami.", - "accessRoleCreateSubmit": "Utwórz rolę", - "accessRoleCreated": "Rola utworzona", - "accessRoleCreatedDescription": "Rola została pomyślnie utworzona.", - "accessRoleErrorCreate": "Nie udało się utworzyć roli", - "accessRoleErrorCreateDescription": "Wystąpił błąd podczas tworzenia roli.", - "accessRoleErrorNewRequired": "Nowa rola jest wymagana", - "accessRoleErrorRemove": "Nie udało się usunąć roli", - "accessRoleErrorRemoveDescription": "Wystąpił błąd podczas usuwania roli.", - "accessRoleName": "Nazwa roli", - "accessRoleQuestionRemove": "Zamierzasz usunąć rolę {name}. Tej akcji nie można cofnąć.", - "accessRoleRemove": "Usuń rolę", - "accessRoleRemoveDescription": "Usuń rolę z organizacji", - "accessRoleRemoveSubmit": "Usuń rolę", - "accessRoleRemoved": "Rola usunięta", - "accessRoleRemovedDescription": "Rola została pomyślnie usunięta.", - "accessRoleRequiredRemove": "Przed usunięciem tej roli, wybierz nową rolę do której zostaną przeniesieni obecni członkowie.", - "manage": "Zarządzaj", - "sitesNotFound": "Nie znaleziono witryn.", - "pangolinServerAdmin": "Administrator serwera - Pangolin", - "licenseTierProfessional": "Licencja Professional", - "licenseTierEnterprise": "Licencja Enterprise", - "licenseTierPersonal": "Personal License", - "licensed": "Licencjonowany", - "yes": "Tak", - "no": "Nie", - "sitesAdditional": "Dodatkowe witryny", - "licenseKeys": "Klucze licencyjne", - "sitestCountDecrease": "Zmniejsz liczbę witryn", - "sitestCountIncrease": "Zwiększ liczbę witryn", - "idpManage": "Zarządzaj dostawcami tożsamości", - "idpManageDescription": "Wyświetl i zarządzaj dostawcami tożsamości w systemie", - "idpDeletedDescription": "Dostawca tożsamości został pomyślnie usunięty", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Czy na pewno chcesz trwale usunąć dostawcę tożsamości {name}?", - "idpMessageRemove": "Spowoduje to usunięcie dostawcy tożsamości i wszystkich powiązanych konfiguracji. Użytkownicy uwierzytelniający się przez tego dostawcę nie będą mogli się już zalogować.", - "idpMessageConfirm": "Aby potwierdzić, wpisz nazwę dostawcy tożsamości poniżej.", - "idpConfirmDelete": "Potwierdź usunięcie dostawcy tożsamości", - "idpDelete": "Usuń dostawcę tożsamości", - "idp": "Dostawcy tożsamości", - "idpSearch": "Szukaj dostawców tożsamości...", - "idpAdd": "Dodaj dostawcę tożsamości", - "idpClientIdRequired": "Identyfikator klienta jest wymagany.", - "idpClientSecretRequired": "Sekret klienta jest wymagany.", - "idpErrorAuthUrlInvalid": "URL autoryzacji musi być prawidłowym adresem URL.", - "idpErrorTokenUrlInvalid": "URL tokena musi być prawidłowym adresem URL.", - "idpPathRequired": "Ścieżka identyfikatora jest wymagana.", - "idpScopeRequired": "Zakresy są wymagane.", - "idpOidcDescription": "Skonfiguruj dostawcę tożsamości OpenID Connect", - "idpCreatedDescription": "Dostawca tożsamości został pomyślnie utworzony", - "idpCreate": "Utwórz dostawcę tożsamości", - "idpCreateDescription": "Skonfiguruj nowego dostawcę tożsamości do uwierzytelniania użytkowników", - "idpSeeAll": "Zobacz wszystkich dostawców tożsamości", - "idpSettingsDescription": "Skonfiguruj podstawowe informacje dla swojego dostawcy tożsamości", - "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": "EE", - "idpType": "Typ dostawcy", - "idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować", - "idpOidcConfigure": "Konfiguracja OAuth2/OIDC", - "idpOidcConfigureDescription": "Skonfiguruj punkty końcowe i poświadczenia dostawcy OAuth2/OIDC", - "idpClientId": "ID klienta", - "idpClientIdDescription": "ID klienta OAuth2 od twojego dostawcy tożsamości", - "idpClientSecret": "Sekret klienta", - "idpClientSecretDescription": "Sekret klienta OAuth2 od twojego dostawcy tożsamości", - "idpAuthUrl": "URL autoryzacji", - "idpAuthUrlDescription": "URL punktu końcowego autoryzacji OAuth2", - "idpTokenUrl": "URL tokena", - "idpTokenUrlDescription": "URL punktu końcowego tokena OAuth2", - "idpOidcConfigureAlert": "Ważna informacja", - "idpOidcConfigureAlertDescription": "Po utworzeniu dostawcy tożsamości, będziesz musiał skonfigurować URL wywołania zwrotnego w ustawieniach swojego dostawcy tożsamości. URL wywołania zwrotnego zostanie podany po pomyślnym utworzeniu.", - "idpToken": "Konfiguracja tokena", - "idpTokenDescription": "Skonfiguruj jak wydobywać informacje o użytkowniku z tokena ID", - "idpJmespathAbout": "O JMESPath", - "idpJmespathAboutDescription": "Poniższe ścieżki używają składni JMESPath do wydobywania wartości z tokena ID.", - "idpJmespathAboutDescriptionLink": "Dowiedz się więcej o JMESPath", - "idpJmespathLabel": "Ścieżka identyfikatora", - "idpJmespathLabelDescription": "JMESPath do identyfikatora użytkownika w tokenie ID", - "idpJmespathEmailPathOptional": "Ścieżka email (Opcjonalnie)", - "idpJmespathEmailPathOptionalDescription": "JMESPath do emaila użytkownika w tokenie ID", - "idpJmespathNamePathOptional": "Ścieżka nazwy (Opcjonalnie)", - "idpJmespathNamePathOptionalDescription": "JMESPath do nazwy użytkownika w tokenie ID", - "idpOidcConfigureScopes": "Zakresy", - "idpOidcConfigureScopesDescription": "Lista zakresów OAuth2 oddzielonych spacjami do żądania", - "idpSubmit": "Utwórz dostawcę tożsamości", - "orgPolicies": "Polityki organizacji", - "idpSettings": "Ustawienia {idpName}", - "idpCreateSettingsDescription": "Skonfiguruj ustawienia dla swojego dostawcy tożsamości", - "roleMapping": "Mapowanie ról", - "orgMapping": "Mapowanie organizacji", - "orgPoliciesSearch": "Szukaj polityk organizacji...", - "orgPoliciesAdd": "Dodaj politykę organizacji", - "orgRequired": "Organizacja jest wymagana", - "error": "Błąd", - "success": "Sukces", - "orgPolicyAddedDescription": "Polityka została pomyślnie dodana", - "orgPolicyUpdatedDescription": "Polityka została pomyślnie zaktualizowana", - "orgPolicyDeletedDescription": "Polityka została pomyślnie usunięta", - "defaultMappingsUpdatedDescription": "Domyślne mapowania zostały pomyślnie zaktualizowane", - "orgPoliciesAbout": "O politykach organizacji", - "orgPoliciesAboutDescription": "Polityki organizacji służą do kontroli dostępu do organizacji na podstawie tokena ID użytkownika. Możesz określić wyrażenia JMESPath do wydobywania informacji o roli i organizacji z tokena ID. Aby dowiedzieć się więcej, zobacz", - "orgPoliciesAboutDescriptionLink": "dokumentację", - "defaultMappingsOptional": "Domyślne mapowania (Opcjonalne)", - "defaultMappingsOptionalDescription": "Domyślne mapowania są używane, gdy nie ma zdefiniowanej polityki organizacji dla organizacji. Możesz tutaj określić domyślne mapowania ról i organizacji.", - "defaultMappingsRole": "Domyślne mapowanie roli", - "defaultMappingsRoleDescription": "JMESPath do wydobycia informacji o roli z tokena ID. Wynik tego wyrażenia musi zwrócić nazwę roli zdefiniowaną w organizacji jako ciąg znaków.", - "defaultMappingsOrg": "Domyślne mapowanie organizacji", - "defaultMappingsOrgDescription": "JMESPath do wydobycia informacji o organizacji z tokena ID. To wyrażenie musi zwrócić ID organizacji lub true, aby użytkownik mógł uzyskać dostęp do organizacji.", - "defaultMappingsSubmit": "Zapisz domyślne mapowania", - "orgPoliciesEdit": "Edytuj politykę organizacji", - "org": "Organizacja", - "orgSelect": "Wybierz organizację", - "orgSearch": "Szukaj organizacji", - "orgNotFound": "Nie znaleziono organizacji.", - "roleMappingPathOptional": "Ścieżka mapowania roli (Opcjonalnie)", - "orgMappingPathOptional": "Ścieżka mapowania organizacji (Opcjonalnie)", - "orgPolicyUpdate": "Aktualizuj politykę", - "orgPolicyAdd": "Dodaj politykę", - "orgPolicyConfig": "Skonfiguruj dostęp dla organizacji", - "idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany", - "redirectUrl": "URL przekierowania", - "redirectUrlAbout": "O URL przekierowania", - "redirectUrlAboutDescription": "Jest to URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten URL w ustawieniach swojego dostawcy tożsamości.", - "pangolinAuth": "Autoryzacja - Pangolin", - "verificationCodeLengthRequirements": "Twój kod weryfikacyjny musi mieć 8 znaków.", - "errorOccurred": "Wystąpił błąd", - "emailErrorVerify": "Nie udało się zweryfikować adresu e-mail:", - "emailVerified": "E-mail został pomyślnie zweryfikowany! Przekierowywanie...", - "verificationCodeErrorResend": "Nie udało się ponownie wysłać kodu weryfikacyjnego:", - "verificationCodeResend": "Kod weryfikacyjny wysłany ponownie", - "verificationCodeResendDescription": "Wysłaliśmy ponownie kod weryfikacyjny na Twój adres e-mail. Sprawdź swoją skrzynkę odbiorczą.", - "emailVerify": "Zweryfikuj e-mail", - "emailVerifyDescription": "Wprowadź kod weryfikacyjny wysłany na Twój adres e-mail.", - "verificationCode": "Kod weryfikacyjny", - "verificationCodeEmailSent": "Wysłaliśmy kod weryfikacyjny na Twój adres e-mail.", - "submit": "Wyślij", - "emailVerifyResendProgress": "Ponowne wysyłanie...", - "emailVerifyResend": "Nie otrzymałeś kodu? Kliknij tutaj, aby wysłać ponownie", - "passwordNotMatch": "Hasła nie są zgodne", - "signupError": "Wystąpił błąd podczas rejestracji", - "pangolinLogoAlt": "Logo Pangolin", - "inviteAlready": "Wygląda na to, że zostałeś już zaproszony!", - "inviteAlreadyDescription": "Aby zaakceptować zaproszenie, musisz się zalogować lub utworzyć konto.", - "signupQuestion": "Masz już konto?", - "login": "Zaloguj się", - "resourceNotFound": "Nie znaleziono zasobu", - "resourceNotFoundDescription": "Zasób, do którego próbujesz uzyskać dostęp, nie istnieje.", - "pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr", - "pincodeRequirementsChars": "PIN może zawierać tylko cyfry", - "passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak", - "passwordRequirementsTitle": "Wymagania dotyczące hasła:", - "passwordRequirementLength": "Przynajmniej 8 znaków długości", - "passwordRequirementUppercase": "Przynajmniej jedna wielka litera", - "passwordRequirementLowercase": "Przynajmniej jedna mała litera", - "passwordRequirementNumber": "Przynajmniej jedna cyfra", - "passwordRequirementSpecial": "Przynajmniej jeden znak specjalny", - "passwordRequirementsMet": "✓ Hasło spełnia wszystkie wymagania", - "passwordStrength": "Siła hasła", - "passwordStrengthWeak": "Słabe", - "passwordStrengthMedium": "Średnie", - "passwordStrengthStrong": "Silne", - "passwordRequirements": "Wymagania:", - "passwordRequirementLengthText": "8+ znaków", - "passwordRequirementUppercaseText": "Wielka litera (A-Z)", - "passwordRequirementLowercaseText": "Mała litera (a-z)", - "passwordRequirementNumberText": "Cyfra (0-9)", - "passwordRequirementSpecialText": "Znak specjalny (!@#$%...)", - "passwordsDoNotMatch": "Hasła nie są zgodne", - "otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak", - "otpEmailSent": "Kod jednorazowy wysłany", - "otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail", - "otpEmailErrorAuthenticate": "Nie udało się uwierzytelnić za pomocą e-maila", - "pincodeErrorAuthenticate": "Nie udało się uwierzytelnić za pomocą kodu PIN", - "passwordErrorAuthenticate": "Nie udało się uwierzytelnić za pomocą hasła", - "poweredBy": "Obsługiwane przez", - "authenticationRequired": "Wymagane uwierzytelnienie", - "authenticationMethodChoose": "Wybierz preferowaną metodę dostępu do {name}", - "authenticationRequest": "Musisz się uwierzytelnić, aby uzyskać dostęp do {name}", - "user": "Użytkownik", - "pincodeInput": "6-cyfrowy kod PIN", - "pincodeSubmit": "Zaloguj się kodem PIN", - "passwordSubmit": "Zaloguj się hasłem", - "otpEmailDescription": "Kod jednorazowy zostanie wysłany na ten adres e-mail.", - "otpEmailSend": "Wyślij kod jednorazowy", - "otpEmail": "Hasło jednorazowe (OTP)", - "otpEmailSubmit": "Wyślij OTP", - "backToEmail": "Powrót do e-maila", - "noSupportKey": "Serwer działa bez klucza wspierającego. Rozważ wsparcie projektu!", - "accessDenied": "Odmowa dostępu", - "accessDeniedDescription": "Nie masz uprawnień dostępu do tego zasobu. Jeśli to pomyłka, skontaktuj się z administratorem.", - "accessTokenError": "Błąd sprawdzania tokena dostępu", - "accessGranted": "Dostęp przyznany", - "accessUrlInvalid": "Nieprawidłowy URL dostępu", - "accessGrantedDescription": "Otrzymałeś dostęp do tego zasobu. Przekierowywanie...", - "accessUrlInvalidDescription": "Ten udostępniony URL dostępu jest nieprawidłowy. Skontaktuj się z właścicielem zasobu, aby otrzymać nowy URL.", - "tokenInvalid": "Nieprawidłowy token", - "pincodeInvalid": "Nieprawidłowy kod", - "passwordErrorRequestReset": "Nie udało się zażądać resetowania:", - "passwordErrorReset": "Nie udało się zresetować hasła:", - "passwordResetSuccess": "Hasło zostało pomyślnie zresetowane! Powrót do logowania...", - "passwordReset": "Zresetuj hasło", - "passwordResetDescription": "Wykonaj kroki, aby zresetować hasło", - "passwordResetSent": "Wyślemy kod resetowania hasła na ten adres e-mail.", - "passwordResetCode": "Kod resetowania", - "passwordResetCodeDescription": "Sprawdź swój e-mail, aby znaleźć kod resetowania.", - "passwordNew": "Nowe hasło", - "passwordNewConfirm": "Potwierdź nowe hasło", - "pincodeAuth": "Kod uwierzytelniający", - "pincodeSubmit2": "Wyślij kod", - "passwordResetSubmit": "Zażądaj resetowania", - "passwordBack": "Powrót do hasła", - "loginBack": "Wróć do logowania", - "signup": "Zarejestruj się", - "loginStart": "Zaloguj się, aby rozpocząć", - "idpOidcTokenValidating": "Walidacja tokena OIDC", - "idpOidcTokenResponse": "Zweryfikuj odpowiedź tokena OIDC", - "idpErrorOidcTokenValidating": "Błąd walidacji tokena OIDC", - "idpConnectingTo": "Łączenie z {name}", - "idpConnectingToDescription": "Weryfikacja tożsamości", - "idpConnectingToProcess": "Łączenie...", - "idpConnectingToFinished": "Połączono", - "idpErrorConnectingTo": "Wystąpił problem z połączeniem z {name}. Skontaktuj się z administratorem.", - "idpErrorNotFound": "Nie znaleziono IdP", - "inviteInvalid": "Nieprawidłowe zaproszenie", - "inviteInvalidDescription": "Link zapraszający jest nieprawidłowy.", - "inviteErrorWrongUser": "Zaproszenie nie jest dla tego użytkownika", - "inviteErrorUserNotExists": "Użytkownik nie istnieje. Najpierw utwórz konto.", - "inviteErrorLoginRequired": "Musisz być zalogowany, aby zaakceptować zaproszenie", - "inviteErrorExpired": "Zaproszenie mogło wygasnąć", - "inviteErrorRevoked": "Zaproszenie mogło zostać odwołane", - "inviteErrorTypo": "W linku zapraszającym może być literówka", - "pangolinSetup": "Konfiguracja - Pangolin", - "orgNameRequired": "Nazwa organizacji jest wymagana", - "orgIdRequired": "ID organizacji jest wymagane", - "orgErrorCreate": "Wystąpił błąd podczas tworzenia organizacji", - "pageNotFound": "Nie znaleziono strony", - "pageNotFoundDescription": "Ups! Strona, której szukasz, nie istnieje.", - "overview": "Przegląd", - "home": "Strona główna", - "accessControl": "Kontrola dostępu", - "settings": "Ustawienia", - "usersAll": "Wszyscy użytkownicy", - "license": "Licencja", - "pangolinDashboard": "Panel - Pangolin", - "noResults": "Nie znaleziono wyników.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Wprowadzone tagi", - "tagsEnteredDescription": "To są wprowadzone przez ciebie tagi.", - "tagsWarnCannotBeLessThanZero": "maxTags i minTags nie mogą być mniejsze od 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag niedozwolony zgodnie z opcjami autouzupełniania", - "tagsWarnInvalid": "Nieprawidłowy tag według validateTag", - "tagWarnTooShort": "Tag {tagText} jest za krótki", - "tagWarnTooLong": "Tag {tagText} jest za długi", - "tagsWarnReachedMaxNumber": "Osiągnięto maksymalną dozwoloną liczbę tagów", - "tagWarnDuplicate": "Zduplikowany tag {tagText} nie został dodany", - "supportKeyInvalid": "Nieprawidłowy klucz", - "supportKeyInvalidDescription": "Twój klucz wspierający jest nieprawidłowy.", - "supportKeyValid": "Prawidłowy klucz", - "supportKeyValidDescription": "Twój klucz wspierający został zweryfikowany. Dziękujemy za wsparcie!", - "supportKeyErrorValidationDescription": "Nie udało się zweryfikować klucza wspierającego.", - "supportKey": "Wesprzyj rozwój i adoptuj Pangolina!", - "supportKeyDescription": "Kup klucz wspierający, aby pomóc nam w dalszym rozwijaniu Pangolina dla społeczności. Twój wkład pozwala nam poświęcić więcej czasu na utrzymanie i dodawanie nowych funkcji do aplikacji dla wszystkich. Nigdy nie wykorzystamy tego do blokowania funkcji za paywallem. Jest to oddzielne od wydania komercyjnego.", - "supportKeyPet": "Będziesz mógł także zaadoptować i poznać swojego własnego zwierzaka Pangolina!", - "supportKeyPurchase": "Płatności są przetwarzane przez GitHub. Następnie możesz pobrać swój klucz na", - "supportKeyPurchaseLink": "naszej stronie", - "supportKeyPurchase2": "i wykorzystać go tutaj.", - "supportKeyLearnMore": "Dowiedz się więcej.", - "supportKeyOptions": "Wybierz opcję, która najbardziej ci odpowiada.", - "supportKetOptionFull": "Pełne wsparcie", - "forWholeServer": "Dla całego serwera", - "lifetimePurchase": "Zakup dożywotni", - "supporterStatus": "Status wspierającego", - "buy": "Kup", - "supportKeyOptionLimited": "Ograniczone wsparcie", - "forFiveUsers": "Dla 5 lub mniej użytkowników", - "supportKeyRedeem": "Wykorzystaj klucz wspierający", - "supportKeyHideSevenDays": "Ukryj na 7 dni", - "supportKeyEnter": "Wprowadź klucz wspierający", - "supportKeyEnterDescription": "Poznaj swojego własnego zwierzaka Pangolina!", - "githubUsername": "Nazwa użytkownika GitHub", - "supportKeyInput": "Klucz wspierający", - "supportKeyBuy": "Kup klucz wspierający", - "logoutError": "Błąd podczas wylogowywania", - "signingAs": "Zalogowany jako", - "serverAdmin": "Administrator serwera", - "managedSelfhosted": "Zarządzane Samodzielnie-Hostingowane", - "otpEnable": "Włącz uwierzytelnianie dwuskładnikowe", - "otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe", - "logout": "Wyloguj się", - "licenseTierProfessionalRequired": "Wymagana edycja Professional", - "licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.", - "actionGetOrg": "Pobierz organizację", - "updateOrgUser": "Aktualizuj użytkownika Org", - "createOrgUser": "Utwórz użytkownika Org", - "actionUpdateOrg": "Aktualizuj organizację", - "actionUpdateUser": "Zaktualizuj użytkownika", - "actionGetUser": "Pobierz użytkownika", - "actionGetOrgUser": "Pobierz użytkownika organizacji", - "actionListOrgDomains": "Lista domen organizacji", - "actionCreateSite": "Utwórz witrynę", - "actionDeleteSite": "Usuń witrynę", - "actionGetSite": "Pobierz witrynę", - "actionListSites": "Lista witryn", - "actionApplyBlueprint": "Zastosuj schemat", - "setupToken": "Skonfiguruj token", - "setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.", - "setupTokenRequired": "Wymagany jest token konfiguracji", - "actionUpdateSite": "Aktualizuj witrynę", - "actionListSiteRoles": "Lista dozwolonych ról witryny", - "actionCreateResource": "Utwórz zasób", - "actionDeleteResource": "Usuń zasób", - "actionGetResource": "Pobierz zasób", - "actionListResource": "Lista zasobów", - "actionUpdateResource": "Aktualizuj zasób", - "actionListResourceUsers": "Lista użytkowników zasobu", - "actionSetResourceUsers": "Ustaw użytkowników zasobu", - "actionSetAllowedResourceRoles": "Ustaw dozwolone role zasobu", - "actionListAllowedResourceRoles": "Lista dozwolonych ról zasobu", - "actionSetResourcePassword": "Ustaw hasło zasobu", - "actionSetResourcePincode": "Ustaw kod PIN zasobu", - "actionSetResourceEmailWhitelist": "Ustaw białą listę email zasobu", - "actionGetResourceEmailWhitelist": "Pobierz białą listę email zasobu", - "actionCreateTarget": "Utwórz cel", - "actionDeleteTarget": "Usuń cel", - "actionGetTarget": "Pobierz cel", - "actionListTargets": "Lista celów", - "actionUpdateTarget": "Aktualizuj cel", - "actionCreateRole": "Utwórz rolę", - "actionDeleteRole": "Usuń rolę", - "actionGetRole": "Pobierz rolę", - "actionListRole": "Lista ról", - "actionUpdateRole": "Aktualizuj rolę", - "actionListAllowedRoleResources": "Lista dozwolonych zasobów roli", - "actionInviteUser": "Zaproś użytkownika", - "actionRemoveUser": "Usuń użytkownika", - "actionListUsers": "Lista użytkowników", - "actionAddUserRole": "Dodaj rolę użytkownika", - "actionGenerateAccessToken": "Wygeneruj token dostępu", - "actionDeleteAccessToken": "Usuń token dostępu", - "actionListAccessTokens": "Lista tokenów dostępu", - "actionCreateResourceRule": "Utwórz regułę zasobu", - "actionDeleteResourceRule": "Usuń regułę zasobu", - "actionListResourceRules": "Lista reguł zasobu", - "actionUpdateResourceRule": "Aktualizuj regułę zasobu", - "actionListOrgs": "Lista organizacji", - "actionCheckOrgId": "Sprawdź ID", - "actionCreateOrg": "Utwórz organizację", - "actionDeleteOrg": "Usuń organizację", - "actionListApiKeys": "Lista kluczy API", - "actionListApiKeyActions": "Lista akcji klucza API", - "actionSetApiKeyActions": "Ustaw dozwolone akcje klucza API", - "actionCreateApiKey": "Utwórz klucz API", - "actionDeleteApiKey": "Usuń klucz API", - "actionCreateIdp": "Utwórz IDP", - "actionUpdateIdp": "Aktualizuj IDP", - "actionDeleteIdp": "Usuń IDP", - "actionListIdps": "Lista IDP", - "actionGetIdp": "Pobierz IDP", - "actionCreateIdpOrg": "Utwórz politykę organizacji IDP", - "actionDeleteIdpOrg": "Usuń politykę organizacji IDP", - "actionListIdpOrgs": "Lista organizacji IDP", - "actionUpdateIdpOrg": "Aktualizuj organizację IDP", - "actionCreateClient": "Utwórz klienta", - "actionDeleteClient": "Usuń klienta", - "actionUpdateClient": "Aktualizuj klienta", - "actionListClients": "Lista klientów", - "actionGetClient": "Pobierz klienta", - "actionCreateSiteResource": "Utwórz zasób witryny", - "actionDeleteSiteResource": "Usuń zasób strony", - "actionGetSiteResource": "Pobierz zasób strony", - "actionListSiteResources": "Lista zasobów strony", - "actionUpdateSiteResource": "Aktualizuj zasób strony", - "actionListInvitations": "Lista zaproszeń", - "noneSelected": "Nie wybrano", - "orgNotFound2": "Nie znaleziono organizacji.", - "searchProgress": "Szukaj...", - "create": "Utwórz", - "orgs": "Organizacje", - "loginError": "Wystąpił błąd podczas logowania", - "passwordForgot": "Zapomniałeś hasła?", - "otpAuth": "Uwierzytelnianie dwuskładnikowe", - "otpAuthDescription": "Wprowadź kod z aplikacji uwierzytelniającej lub jeden z jednorazowych kodów zapasowych.", - "otpAuthSubmit": "Wyślij kod", - "idpContinue": "Lub kontynuuj z", - "otpAuthBack": "Powrót do logowania", - "navbar": "Menu nawigacyjne", - "navbarDescription": "Główne menu nawigacyjne aplikacji", - "navbarDocsLink": "Dokumentacja", - "otpErrorEnable": "Nie można włączyć 2FA", - "otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA", - "otpSetupCheckCode": "Wprowadź 6-cyfrowy kod", - "otpSetupCheckCodeRetry": "Nieprawidłowy kod. Spróbuj ponownie.", - "otpSetup": "Włącz uwierzytelnianie dwuskładnikowe", - "otpSetupDescription": "Zabezpiecz swoje konto dodatkową warstwą ochrony", - "otpSetupScanQr": "Zeskanuj ten kod QR za pomocą aplikacji uwierzytelniającej lub wprowadź klucz tajny ręcznie:", - "otpSetupSecretCode": "Kod uwierzytelniający", - "otpSetupSuccess": "Włączono uwierzytelnianie dwuskładnikowe", - "otpSetupSuccessStoreBackupCodes": "Twoje konto jest teraz bezpieczniejsze. Nie zapomnij zapisać kodów zapasowych.", - "otpErrorDisable": "Nie można wyłączyć 2FA", - "otpErrorDisableDescription": "Wystąpił błąd podczas wyłączania 2FA", - "otpRemove": "Wyłącz uwierzytelnianie dwuskładnikowe", - "otpRemoveDescription": "Wyłącz uwierzytelnianie dwuskładnikowe dla swojego konta", - "otpRemoveSuccess": "Wyłączono uwierzytelnianie dwuskładnikowe", - "otpRemoveSuccessMessage": "Uwierzytelnianie dwuskładnikowe zostało wyłączone dla Twojego konta. Możesz je włączyć ponownie w dowolnym momencie.", - "otpRemoveSubmit": "Wyłącz 2FA", - "paginator": "Strona {current} z {last}", - "paginatorToFirst": "Przejdź do pierwszej strony", - "paginatorToPrevious": "Przejdź do poprzedniej strony", - "paginatorToNext": "Przejdź do następnej strony", - "paginatorToLast": "Przejdź do ostatniej strony", - "copyText": "Kopiuj tekst", - "copyTextFailed": "Nie udało się skopiować tekstu: ", - "copyTextClipboard": "Kopiuj do schowka", - "inviteErrorInvalidConfirmation": "Nieprawidłowe potwierdzenie", - "passwordRequired": "Hasło jest wymagane", - "allowAll": "Zezwól wszystkim", - "permissionsAllowAll": "Zezwól na wszystkie uprawnienia", - "githubUsernameRequired": "Nazwa użytkownika GitHub jest wymagana", - "supportKeyRequired": "Klucz wspierający jest wymagany", - "passwordRequirementsChars": "Hasło musi mieć co najmniej 8 znaków", - "language": "Język", - "verificationCodeRequired": "Kod jest wymagany", - "userErrorNoUpdate": "Brak użytkownika do aktualizacji", - "siteErrorNoUpdate": "Brak witryny do aktualizacji", - "resourceErrorNoUpdate": "Brak zasobu do aktualizacji", - "authErrorNoUpdate": "Brak danych uwierzytelniania do aktualizacji", - "orgErrorNoUpdate": "Brak organizacji do aktualizacji", - "orgErrorNoProvided": "Nie podano organizacji", - "apiKeysErrorNoUpdate": "Brak klucza API do aktualizacji", - "sidebarOverview": "Przegląd", - "sidebarHome": "Strona główna", - "sidebarSites": "Witryny", - "sidebarResources": "Zasoby", - "sidebarAccessControl": "Kontrola dostępu", - "sidebarUsers": "Użytkownicy", - "sidebarInvitations": "Zaproszenia", - "sidebarRoles": "Role", - "sidebarShareableLinks": "Linki do udostępnienia", - "sidebarApiKeys": "Klucze API", - "sidebarSettings": "Ustawienia", - "sidebarAllUsers": "Wszyscy użytkownicy", - "sidebarIdentityProviders": "Dostawcy tożsamości", - "sidebarLicense": "Licencja", - "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.", - "enableDockerSocketLink": "Dowiedz się więcej", - "viewDockerContainers": "Zobacz kontenery dokujące", - "containersIn": "Pojemniki w {siteName}", - "selectContainerDescription": "Wybierz dowolny kontener do użycia jako nazwa hosta dla tego celu. Kliknij port, aby użyć portu.", - "containerName": "Nazwa", - "containerImage": "Obraz", - "containerState": "Stan", - "containerNetworks": "Sieci", - "containerHostnameIp": "Nazwa hosta/IP", - "containerLabels": "Etykiety", - "containerLabelsCount": "{count, plural, one {# etykieta} few {# etykiety} many {# etykiet} other {# etykiet}}", - "containerLabelsTitle": "Etykiety kontenera", - "containerLabelEmpty": "", - "containerPorts": "Porty", - "containerPortsMore": "+{count} więcej", - "containerActions": "Akcje", - "select": "Wybierz", - "noContainersMatchingFilters": "Nie znaleziono kontenerów pasujących do obecnych filtrów.", - "showContainersWithoutPorts": "Pokaż kontenery bez portów", - "showStoppedContainers": "Pokaż zatrzymane kontenery", - "noContainersFound": "Nie znaleziono kontenerów. Upewnij się, że kontenery dokujące są uruchomione.", - "searchContainersPlaceholder": "Szukaj w {count} kontenerach...", - "searchResultsCount": "{count, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyników}}", - "filters": "Filtry", - "filterOptions": "Opcje filtru", - "filterPorts": "Porty", - "filterStopped": "Zatrzymano", - "clearAllFilters": "Wyczyść wszystkie filtry", - "columns": "Kwota, którą należy zgłosić w kolumnie 060 tego wiersza: pierwotne odliczenie, art. 36 ust. 1 lit. b) CRR.", - "toggleColumns": "Przełącz kolumny", - "refreshContainersList": "Odśwież listę kontenerów", - "searching": "Wyszukiwanie...", - "noContainersFoundMatching": "Nie znaleziono kontenerów pasujących do \"{filter}\".", - "light": "jasny", - "dark": "ciemny", - "system": "System", - "theme": "Motyw", - "subnetRequired": "Podsieć jest wymagana", - "initialSetupTitle": "Wstępna konfiguracja serwera", - "initialSetupDescription": "Utwórz początkowe konto administratora serwera. Może istnieć tylko jeden administrator serwera. Zawsze można zmienić te dane uwierzytelniające.", - "createAdminAccount": "Utwórz konto administratora", - "setupErrorCreateAdmin": "Wystąpił błąd podczas tworzenia konta administratora serwera.", - "certificateStatus": "Status certyfikatu", - "loading": "Ładowanie", - "restart": "Uruchom ponownie", - "domains": "Domeny", - "domainsDescription": "Zarządzaj domenami swojej organizacji", - "domainsSearch": "Szukaj domen...", - "domainAdd": "Dodaj domenę", - "domainAddDescription": "Zarejestruj nową domenę w swojej organizacji", - "domainCreate": "Utwórz domenę", - "domainCreatedDescription": "Domena utworzona pomyślnie", - "domainDeletedDescription": "Domena usunięta pomyślnie", - "domainQuestionRemove": "Czy na pewno chcesz usunąć domenę {domain} ze swojego konta?", - "domainMessageRemove": "Po usunięciu domena nie będzie już powiązana z twoim kontem.", - "domainMessageConfirm": "Aby potwierdzić, wpisz nazwę domeny poniżej.", - "domainConfirmDelete": "Potwierdź usunięcie domeny", - "domainDelete": "Usuń domenę", - "domain": "Domena", - "selectDomainTypeNsName": "Delegacja domeny (NS)", - "selectDomainTypeNsDescription": "Ta domena i wszystkie jej subdomeny. Użyj tego, gdy chcesz kontrolować całą strefę domeny.", - "selectDomainTypeCnameName": "Pojedyncza domena (CNAME)", - "selectDomainTypeCnameDescription": "Tylko ta pojedyncza domena. Użyj tego dla poszczególnych subdomen lub wpisów specyficznych dla domeny.", - "selectDomainTypeWildcardName": "Domena wieloznaczna", - "selectDomainTypeWildcardDescription": "Ta domena i jej subdomeny.", - "domainDelegation": "Pojedyncza domena", - "selectType": "Wybierz typ", - "actions": "Akcje", - "refresh": "Odśwież", - "refreshError": "Nie udało się odświeżyć danych", - "verified": "Zatwierdzony", - "pending": "Oczekuje", - "sidebarBilling": "Fakturowanie", - "billing": "Fakturowanie", - "orgBillingDescription": "Zarządzaj swoimi informacjami rozliczeniowymi i subskrypcjami", - "github": "GitHub", - "pangolinHosted": "Logo Pangolin", - "fossorial": "Fossorial", - "completeAccountSetup": "Zakończ konfigurację konta", - "completeAccountSetupDescription": "Ustaw swoje hasło, aby rozpocząć", - "accountSetupSent": "Wyślemy kod konfiguracji konta na ten adres e-mail.", - "accountSetupCode": "Kod konfiguracji", - "accountSetupCodeDescription": "Sprawdź swój e-mail, aby znaleźć kod konfiguracji.", - "passwordCreate": "Utwórz hasło", - "passwordCreateConfirm": "Potwierdź hasło", - "accountSetupSubmit": "Wyślij kod konfiguracji", - "completeSetup": "Zakończ konfigurację", - "accountSetupSuccess": "Konfiguracja konta zakończona! Witaj w Pangolin!", - "documentation": "Dokumentacja", - "saveAllSettings": "Zapisz wszystkie ustawienia", - "settingsUpdated": "Ustawienia zaktualizowane", - "settingsUpdatedDescription": "Wszystkie ustawienia zostały pomyślnie zaktualizowane", - "settingsErrorUpdate": "Nie udało się zaktualizować ustawień", - "settingsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień", - "sidebarCollapse": "Zwiń", - "sidebarExpand": "Rozwiń", - "newtUpdateAvailable": "Dostępna aktualizacja", - "newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.", - "domainPickerEnterDomain": "Domena", - "domainPickerPlaceholder": "mojapp.example.com", - "domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.", - "domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje", - "domainPickerTabAll": "Wszystko", - "domainPickerTabOrganization": "Organizacja", - "domainPickerTabProvided": "Dostarczona", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Sprawdzanie dostępności...", - "domainPickerNoMatchingDomains": "Nie znaleziono pasujących domen. Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.", - "domainPickerOrganizationDomains": "Domeny organizacji", - "domainPickerProvidedDomains": "Dostarczone domeny", - "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", - "createNewOrgDescription": "Utwórz nową organizację", - "organization": "Organizacja", - "port": "Port", - "securityKeyManage": "Zarządzaj kluczami bezpieczeństwa", - "securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła", - "securityKeyRegister": "Zarejestruj nowy klucz bezpieczeństwa", - "securityKeyList": "Twoje klucze bezpieczeństwa", - "securityKeyNone": "Brak zarejestrowanych kluczy bezpieczeństwa", - "securityKeyNameRequired": "Nazwa jest wymagana", - "securityKeyRemove": "Usuń", - "securityKeyLastUsed": "Ostatnio używany: {date}", - "securityKeyNameLabel": "Nazwa", - "securityKeyRegisterSuccess": "Klucz bezpieczeństwa został pomyślnie zarejestrowany", - "securityKeyRegisterError": "Błąd podczas rejestracji klucza bezpieczeństwa", - "securityKeyRemoveSuccess": "Klucz bezpieczeństwa został pomyślnie usunięty", - "securityKeyRemoveError": "Błąd podczas usuwania klucza bezpieczeństwa", - "securityKeyLoadError": "Błąd podczas ładowania kluczy bezpieczeństwa", - "securityKeyLogin": "Zaloguj się kluczem bezpieczeństwa", - "securityKeyAuthError": "Błąd podczas uwierzytelniania kluczem bezpieczeństwa", - "securityKeyRecommendation": "Rozważ zarejestrowanie innego klucza bezpieczeństwa na innym urządzeniu, aby upewnić się, że nie zostaniesz zablokowany z dostępu do swojego konta.", - "registering": "Rejestracja...", - "securityKeyPrompt": "Proszę zweryfikować swoją tożsamość, używając klucza bezpieczeństwa. Upewnij się, że twój klucz bezpieczeństwa jest podłączony i gotowy.", - "securityKeyBrowserNotSupported": "Twoja przeglądarka nie obsługuje kluczy bezpieczeństwa. Proszę użyć nowoczesnej przeglądarki, takiej jak Chrome, Firefox lub Safari.", - "securityKeyPermissionDenied": "Proszę umożliwić dostęp do klucza bezpieczeństwa, aby kontynuować logowanie.", - "securityKeyRemovedTooQuickly": "Proszę utrzymać klucz bezpieczeństwa podłączony, dopóki proces logowania się nie zakończy.", - "securityKeyNotSupported": "Twój klucz bezpieczeństwa może być niekompatybilny. Proszę spróbować innego klucza bezpieczeństwa.", - "securityKeyUnknownError": "Wystąpił problem z używaniem klucza bezpieczeństwa. Proszę spróbować ponownie.", - "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ć.", - "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ć", - "securityKeyTwoFactorRequired": "Wymagane uwierzytelnianie dwuskładnikowe", - "securityKeyTwoFactorDescription": "Proszę wprowadzić kod uwierzytelnienia dwuskładnikowego, aby zarejestrować klucz bezpieczeństwa", - "securityKeyTwoFactorRemoveDescription": "Proszę wprowadzić kod uwierzytelnienia dwuskładnikowego, aby usunąć klucz bezpieczeństwa", - "securityKeyTwoFactorCode": "Kod dwuskładnikowy", - "securityKeyRemoveTitle": "Usuń klucz bezpieczeństwa", - "securityKeyRemoveDescription": "Wprowadź hasło, aby usunąć klucz bezpieczeństwa \"{name}\"", - "securityKeyNoKeysRegistered": "Nie zarejestrowano kluczy bezpieczeństwa", - "securityKeyNoKeysDescription": "Dodaj klucz bezpieczeństwa, aby zwiększyć swoje zabezpieczenia konta", - "createDomainRequired": "Domena jest wymagana", - "createDomainAddDnsRecords": "Dodaj rekordy DNS", - "createDomainAddDnsRecordsDescription": "Dodaj poniższe rekordy DNS do swojego dostawcy domeny, aby zakończyć konfigurację.", - "createDomainNsRecords": "Rekordy NS", - "createDomainRecord": "Rekord", - "createDomainType": "Typ:", - "createDomainName": "Nazwa:", - "createDomainValue": "Wartość:", - "createDomainCnameRecords": "Rekordy CNAME", - "createDomainARecords": "Rekordy A", - "createDomainRecordNumber": "Rekord {number}", - "createDomainTxtRecords": "Rekordy TXT", - "createDomainSaveTheseRecords": "Zapisz te rekordy", - "createDomainSaveTheseRecordsDescription": "Upewnij się, że zapiszesz te rekordy DNS, ponieważ nie będziesz mieć ich ponownie na ekranie.", - "createDomainDnsPropagation": "Propagacja DNS", - "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", - "and": "oraz", - "privacyPolicy": "polityką prywatności" - }, - "siteRequired": "Strona jest wymagana.", - "olmTunnel": "Tunel Olm", - "olmTunnelDescription": "Użyj Olm do łączności klienta", - "errorCreatingClient": "Błąd podczas tworzenia klienta", - "clientDefaultsNotFound": "Nie znaleziono domyślnych ustawień klienta", - "createClient": "Utwórz Klienta", - "createClientDescription": "Utwórz nowego klienta do łączenia się z Twoimi witrynami", - "seeAllClients": "Zobacz Wszystkich Klientów", - "clientInformation": "Informacje o Kliencie", - "clientNamePlaceholder": "Nazwa klienta", - "address": "Adres", - "subnetPlaceholder": "Podsieć", - "addressDescription": "Adres, którego ten klient będzie używać do łączności", - "selectSites": "Wybierz witryny", - "sitesDescription": "Klient będzie miał łączność z wybranymi witrynami", - "clientInstallOlm": "Zainstaluj Olm", - "clientInstallOlmDescription": "Uruchom Olm na swoim systemie", - "clientOlmCredentials": "Poświadczenia Olm", - "clientOlmCredentialsDescription": "To jest sposób, w jaki Olm będzie się uwierzytelniać z serwerem", - "olmEndpoint": "Punkt Końcowy Olm", - "olmId": "Identyfikator Olm", - "olmSecretKey": "Tajny Klucz Olm", - "clientCredentialsSave": "Zapisz swoje poświadczenia", - "clientCredentialsSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.", - "generalSettingsDescription": "Skonfiguruj ogólne ustawienia dla tego klienta", - "clientUpdated": "Klient zaktualizowany", - "clientUpdatedDescription": "Klient został zaktualizowany.", - "clientUpdateFailed": "Nie udało się zaktualizować klienta", - "clientUpdateError": "Wystąpił błąd podczas aktualizacji klienta.", - "sitesFetchFailed": "Nie udało się pobrać witryn", - "sitesFetchError": "Wystąpił błąd podczas pobierania witryn.", - "olmErrorFetchReleases": "Wystąpił błąd podczas pobierania wydań Olm.", - "olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.", - "remoteSubnets": "Zdalne Podsieci", - "enterCidrRange": "Wprowadź zakres CIDR", - "remoteSubnetsDescription": "Dodaj zakresy CIDR, które można uzyskać zdalnie z tej strony za pomocą klientów. Użyj formatu jak 10.0.0.0/24. Dotyczy to WYŁĄCZNIE łączności klienta VPN.", - "resourceEnableProxy": "Włącz publiczny proxy", - "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", - "externalProxyEnabled": "Zewnętrzny Proxy Włączony", - "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", - "domainPickerBaseDomainLabel": "Domen bazowa", - "domainPickerSearchDomains": "Szukaj domen...", - "domainPickerNoDomainsFound": "Nie znaleziono domen", - "domainPickerLoadingDomains": "Ładowanie domen...", - "domainPickerSelectBaseDomain": "Wybierz domenę bazową...", - "domainPickerNotAvailableForCname": "Niedostępne dla domen CNAME", - "domainPickerEnterSubdomainOrLeaveBlank": "Wprowadź poddomenę lub pozostaw puste, aby użyć domeny bazowej.", - "domainPickerEnterSubdomainToSearch": "Wprowadź poddomenę, aby wyszukać i wybrać z dostępnych darmowych domen.", - "domainPickerFreeDomains": "Darmowe domeny", - "domainPickerSearchForAvailableDomains": "Szukaj dostępnych domen", - "domainPickerNotWorkSelfHosted": "Uwaga: Darmowe domeny nie są obecnie dostępne dla instancji samodzielnie-hostowanych.", - "resourceDomain": "Domena", - "resourceEditDomain": "Edytuj domenę", - "siteName": "Nazwa strony", - "proxyPort": "Port", - "resourcesTableProxyResources": "Zasoby proxy", - "resourcesTableClientResources": "Zasoby klienta", - "resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.", - "resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.", - "resourcesTableDestination": "Miejsce docelowe", - "resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z", - "resourcesTableClients": "Klientami", - "resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.", - "editInternalResourceDialogEditClientResource": "Edytuj zasób klienta", - "editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.", - "editInternalResourceDialogResourceProperties": "Właściwości zasobów", - "editInternalResourceDialogName": "Nazwa", - "editInternalResourceDialogProtocol": "Protokół", - "editInternalResourceDialogSitePort": "Port witryny", - "editInternalResourceDialogTargetConfiguration": "Konfiguracja celu", - "editInternalResourceDialogCancel": "Anuluj", - "editInternalResourceDialogSaveResource": "Zapisz zasób", - "editInternalResourceDialogSuccess": "Sukces", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Wewnętrzny zasób zaktualizowany pomyślnie", - "editInternalResourceDialogError": "Błąd", - "editInternalResourceDialogFailedToUpdateInternalResource": "Nie udało się zaktualizować wewnętrznego zasobu", - "editInternalResourceDialogNameRequired": "Nazwa jest wymagana", - "editInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków", - "editInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1", - "editInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", - "editInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", - "editInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", - "createInternalResourceDialogNoSitesAvailable": "Brak dostępnych stron", - "createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną stronę Newt z skonfigurowanym podsiecią, aby tworzyć wewnętrzne zasoby.", - "createInternalResourceDialogClose": "Zamknij", - "createInternalResourceDialogCreateClientResource": "Utwórz zasób klienta", - "createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny dla klientów połączonych z wybraną stroną.", - "createInternalResourceDialogResourceProperties": "Właściwości zasobów", - "createInternalResourceDialogName": "Nazwa", - "createInternalResourceDialogSite": "Witryna", - "createInternalResourceDialogSelectSite": "Wybierz stronę...", - "createInternalResourceDialogSearchSites": "Szukaj stron...", - "createInternalResourceDialogNoSitesFound": "Nie znaleziono stron.", - "createInternalResourceDialogProtocol": "Protokół", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Port witryny", - "createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.", - "createInternalResourceDialogTargetConfiguration": "Konfiguracja celu", - "createInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.", - "createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.", - "createInternalResourceDialogCancel": "Anuluj", - "createInternalResourceDialogCreateResource": "Utwórz zasób", - "createInternalResourceDialogSuccess": "Sukces", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Wewnętrzny zasób utworzony pomyślnie", - "createInternalResourceDialogError": "Błąd", - "createInternalResourceDialogFailedToCreateInternalResource": "Nie udało się utworzyć wewnętrznego zasobu", - "createInternalResourceDialogNameRequired": "Nazwa jest wymagana", - "createInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków", - "createInternalResourceDialogPleaseSelectSite": "Proszę wybrać stronę", - "createInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1", - "createInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP", - "createInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1", - "createInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536", - "siteConfiguration": "Konfiguracja", - "siteAcceptClientConnections": "Akceptuj połączenia klienta", - "siteAcceptClientConnectionsDescription": "Pozwól innym urządzeniom połączyć się przez tę instancję Newt jako bramę za pomocą klientów.", - "siteAddress": "Adres strony", - "siteAddressDescription": "Podaj adres IP hosta, do którego klienci będą się łączyć. Jest to wewnętrzny adres strony w sieci Pangolin dla klientów do adresowania. Musi zawierać się w podsieci organizacji.", - "autoLoginExternalIdp": "Automatyczny login z zewnętrznym IDP", - "autoLoginExternalIdpDescription": "Natychmiastowe przekierowanie użytkownika do zewnętrznego IDP w celu uwierzytelnienia.", - "selectIdp": "Wybierz IDP", - "selectIdpPlaceholder": "Wybierz IDP...", - "selectIdpRequired": "Proszę wybrać IDP, gdy aktywne jest automatyczne logowanie.", - "autoLoginTitle": "Przekierowywanie", - "autoLoginDescription": "Przekierowanie do zewnętrznego dostawcy tożsamości w celu uwierzytelnienia.", - "autoLoginProcessing": "Przygotowywanie uwierzytelniania...", - "autoLoginRedirecting": "Przekierowanie do logowania...", - "autoLoginError": "Błąd automatycznego logowania", - "autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.", - "autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.", - "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", - "introTitle": "Zarządzany samowystarczalny Pangolin", - "introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.", - "introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin — tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:", - "benefitSimplerOperations": { - "title": "Uproszczone operacje", - "description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju." - }, - "benefitAutomaticUpdates": { - "title": "Automatyczne aktualizacje", - "description": "Panel chmury rozwija się szybko, więc otrzymujesz nowe funkcje i poprawki błędów bez konieczności ręcznego ciągnięcia nowych kontenerów za każdym razem." - }, - "benefitLessMaintenance": { - "title": "Mniej konserwacji", - "description": "Brak migracji bazy danych, kopii zapasowych lub dodatkowej infrastruktury do zarządzania. Obsługujemy to w chmurze." - }, - "benefitCloudFailover": { - "title": "Przegrywanie w chmurze", - "description": "Jeśli Twój węzeł zostanie wyłączony, tunele mogą tymczasowo zawieść do naszych punktów w chmurze, dopóki nie przyniesiesz go z powrotem do trybu online." - }, - "benefitHighAvailability": { - "title": "Wysoka dostępność (PoPs)", - "description": "Możesz również dołączyć wiele węzłów do swojego konta w celu nadmiarowości i lepszej wydajności." - }, - "benefitFutureEnhancements": { - "title": "Przyszłe ulepszenia", - "description": "Planujemy dodać więcej narzędzi analitycznych, ostrzegawczych i zarządzania, aby zwiększyć odporność wdrożenia." - }, - "docsAlert": { - "text": "Dowiedz się więcej o opcji zarządzania samodzielnym hostingiem w naszym", - "documentation": "dokumentacja" - }, - "convertButton": "Konwertuj ten węzeł do zarządzanego samodzielnie" - }, - "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", - "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", - "domainPickerUnverified": "Niezweryfikowane", - "domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.", - "domainPickerError": "Błąd", - "domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji", - "domainPickerErrorCheckAvailability": "Nie udało się sprawdzić dostępności domeny", - "domainPickerInvalidSubdomain": "Nieprawidłowa subdomena", - "domainPickerInvalidSubdomainRemoved": "Wejście \"{sub}\" zostało usunięte, ponieważ jest nieprawidłowe.", - "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", - "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 deleted file mode 100644 index 0a93a357..00000000 --- a/messages/pt-PT.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Crie sua organização, site e recursos", - "setupNewOrg": "Nova organização", - "setupCreateOrg": "Criar Organização", - "setupCreateResources": "Criar recursos", - "setupOrgName": "Nome Da Organização", - "orgDisplayName": "Este é o nome de exibição da sua organização.", - "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": "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": "É 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": "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 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": "Voltar ao inicio", - "inviteLogInOtherUser": "Fazer login como um utilizador diferente", - "createAnAccount": "Crie uma conta", - "inviteNotAccepted": "Convite não aceite", - "authCreateAccount": "Crie uma conta para começar", - "authNoAccount": "Não possui uma conta?", - "email": "e-mail", - "password": "Palavra-passe", - "confirmPassword": "Confirmar senha", - "createAccount": "Criar conta", - "viewSettings": "Visualizar configurações", - "delete": "apagar", - "name": "Nome:", - "online": "Disponível", - "offline": "Desconectado", - "site": "site", - "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 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": "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", - "siteCreateDescription": "Crie um novo site para começar a conectar seus recursos", - "close": "FECHAR", - "siteErrorCreate": "Erro ao criar site", - "siteErrorCreateKeyPair": "Par de chaves ou padrões do site não encontrados", - "siteErrorCreateDefaults": "Padrão do site não encontrado", - "method": "Método", - "siteMethodDescription": "É assim que você irá expor as conexões.", - "siteLearnNewt": "Saiba como instalar o Newt no seu sistema", - "siteSeeConfigOnce": "Você só poderá ver a configuração uma vez.", - "siteLoadWGConfig": "Carregando configuração do WireGuarde...", - "siteDocker": "Expandir para detalhes da implantação Docker", - "toggle": "Alternador", - "dockerCompose": "Composição do Docker", - "dockerRun": "Execução do Docker", - "siteLearnLocal": "Os sites locais não são túneis, saiba mais", - "siteConfirmCopy": "Eu copiei a configuração", - "searchSitesProgress": "Procurar sites...", - "siteAdd": "Adicionar Site", - "siteInstallNewt": "Instalar Novo", - "siteInstallNewtDescription": "Novo item em execução no seu sistema", - "WgConfiguration": "Configuração do WireGuard", - "WgConfigurationDescription": "Use a seguinte configuração para conectar-se à sua rede", - "operatingSystem": "Sistema operacional", - "commands": "Comandos", - "recommended": "Recomendados", - "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 apagar site", - "siteErrorUpdate": "Falha ao atualizar site", - "siteErrorUpdateDescription": "Ocorreu um erro ao atualizar o site.", - "siteUpdated": "Site atualizado", - "siteUpdatedDescription": "O site foi atualizado.", - "siteGeneralDescription": "Configurar as configurações gerais para este site", - "siteSettingDescription": "Configure as configurações no seu site", - "siteSetting": "Configurações do {siteName}", - "siteNewtTunnel": "Novo túnel (recomendado)", - "siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.", - "siteWg": "WireGuard Básico", - "siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.", - "siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", - "siteLocalDescription": "Recursos locais apenas. Sem túneis.", - "siteLocalDescriptionSaas": "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", - "siteNewtCredentialsDescription": "É assim que o novo sistema se autenticará com o servidor", - "siteCredentialsSave": "Salve suas credenciais", - "siteCredentialsSaveDescription": "Você só será capaz de ver esta vez. Certifique-se de copiá-lo para um lugar seguro.", - "siteInfo": "Informações do Site", - "status": "SItuação", - "shareTitle": "Gerir links partilhados", - "shareDescription": "Criar links compartilháveis para conceder acesso temporário ou permanente aos seus recursos", - "shareSearch": "Pesquisar links de compartilhamento...", - "shareCreate": "Criar Link de Compartilhamento", - "shareErrorDelete": "Falha ao apagar o link", - "shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link", - "shareDeleted": "Link excluído", - "shareDeletedDescription": "O link foi eliminado", - "shareTokenDescription": "Seu token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.", - "accessToken": "Token de acesso", - "usageExamples": "Exemplos de uso", - "tokenId": "ID do Token", - "requestHeades": "Cabeçalhos de solicitação", - "queryParameter": "Parâmetro de consulta", - "importantNote": "Nota importante", - "shareImportantDescription": "Por razões de segurança, o uso de cabeçalhos é recomendado através dos parâmetros de consulta quando possível, já que os parâmetros de consulta podem estar logados nos logs do servidor ou no histórico do navegador.", - "token": "Identificador", - "shareTokenSecurety": "Mantenha seu token de acesso seguro. Não o compartilhe em áreas de acesso público ou código do lado do cliente.", - "shareErrorFetchResource": "Falha ao buscar recursos", - "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 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 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 aceder o recurso. Compartilhe com cuidado.", - "shareTokenUsage": "Ver Uso do Token de Acesso", - "createLink": "Criar Link", - "resourcesNotFound": "Nenhum recurso encontrado", - "resourceSearch": "Recursos de pesquisa", - "openMenu": "Abrir menu", - "resource": "Recurso", - "title": "Título", - "created": "Criado", - "expires": "Expira", - "never": "nunca", - "shareErrorSelectResource": "Por favor, selecione um recurso", - "resourceTitle": "Gerir Recursos", - "resourceDescription": "Crie proxies seguros para seus aplicativos privados", - "resourcesSearch": "Procurar recursos...", - "resourceAdd": "Adicionar Recurso", - "resourceErrorDelte": "Erro ao apagar recurso", - "authentication": "Autenticação", - "protected": "Protegido", - "notProtected": "Não Protegido", - "resourceMessageRemove": "Uma vez removido, o recurso não estará mais acessível. Todos os alvos associados ao recurso também serão removidos.", - "resourceMessageConfirm": "Para confirmar, por favor, digite o nome do recurso abaixo.", - "resourceQuestionRemove": "Tem certeza que deseja remover o recurso {selectedResource} da organização?", - "resourceHTTP": "Recurso HTTPS", - "resourceHTTPDescription": "O proxy solicita ao seu aplicativo via HTTPS usando um subdomínio ou domínio base.", - "resourceRaw": "Recurso TCP/UDP bruto", - "resourceRawDescription": "O proxy solicita ao seu aplicativo sobre TCP/UDP usando um número de porta.", - "resourceCreate": "Criar Recurso", - "resourceCreateDescription": "Siga os passos abaixo para criar um novo recurso", - "resourceSeeAll": "Ver todos os recursos", - "resourceInfo": "Informação do recurso", - "resourceNameDescription": "Este é o nome de exibição para o recurso.", - "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 aceder seu recurso", - "resourceHTTPSSettings": "Configurações de HTTPS", - "resourceHTTPSSettingsDescription": "Configure como seu recurso será acessado por HTTPS", - "domainType": "Tipo de domínio", - "subdomain": "Subdomínio", - "baseDomain": "Domínio Base", - "subdomnainDescription": "O subdomínio onde seu recurso estará acessível.", - "resourceRawSettings": "Configurações TCP/UDP", - "resourceRawSettingsDescription": "Configure como seu recurso será acessado sobre TCP/UDP", - "protocol": "Protocolo", - "protocolSelect": "Selecione um protocolo", - "resourcePortNumber": "Número da Porta", - "resourcePortNumberDescription": "O número da porta externa para requisições de proxy.", - "cancel": "cancelar", - "resourceConfig": "Snippets de Configuração", - "resourceConfigDescription": "Copie e cole estes snippets de configuração para configurar o seu recurso TCP/UDP", - "resourceAddEntrypoints": "Traefik: Adicionar pontos de entrada", - "resourceExposePorts": "Gerbil: Expor Portas no Docker Compose", - "resourceLearnRaw": "Aprenda como configurar os recursos TCP/UDP", - "resourceBack": "Voltar aos recursos", - "resourceGoTo": "Ir para o Recurso", - "resourceDelete": "Excluir Recurso", - "resourceDeleteConfirm": "Confirmar que pretende apagar o recurso", - "visibility": "Visibilidade", - "enabled": "Ativado", - "disabled": "Desabilitado", - "general": "Gerais", - "generalSettings": "Configurações Gerais", - "proxy": "Proxy", - "internal": "Interno", - "rules": "Regras", - "resourceSettingDescription": "Configure as configurações do seu recurso", - "resourceSetting": "Configurações do {resourceName}", - "alwaysAllow": "Sempre permitir", - "alwaysDeny": "Sempre negar", - "passToAuth": "Passar para Autenticação", - "orgSettingsDescription": "Configurar as configurações gerais da sua organização", - "orgGeneralSettings": "Configurações da organização", - "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 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", - "orgUpdatedDescription": "A organização foi atualizada.", - "orgErrorUpdate": "Falha ao atualizar organização", - "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 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": "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 utilizador", - "username": "Usuário:", - "identityProvider": "Provedor de Identidade", - "role": "Funções", - "nameRequired": "O nome é obrigatório", - "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": "Gerir seus convites para outros utilizadores", - "inviteSearch": "Procurar convites...", - "minutes": "minutos", - "hours": "horas", - "days": "dias", - "weeks": "semanas", - "months": "Meses", - "years": "anos", - "day": "{count, plural, one {# dia} other {# dias}}", - "apiKeysTitle": "Informações da Chave API", - "apiKeysConfirmCopy2": "Você deve confirmar que copiou a chave API.", - "apiKeysErrorCreate": "Erro ao criar chave API", - "apiKeysErrorSetPermission": "Erro ao definir permissões", - "apiKeysCreate": "Gerar Chave API", - "apiKeysCreateDescription": "Gerar uma nova chave API para sua organização", - "apiKeysGeneralSettings": "Permissões", - "apiKeysGeneralSettingsDescription": "Determine o que esta chave API pode fazer", - "apiKeysList": "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", - "generate": "Gerar", - "done": "Concluído", - "apiKeysSeeAll": "Ver Todas as Chaves API", - "apiKeysPermissionsErrorLoadingActions": "Erro ao carregar ações da chave API", - "apiKeysPermissionsErrorUpdate": "Erro ao definir permissões", - "apiKeysPermissionsUpdated": "Permissões atualizadas", - "apiKeysPermissionsUpdatedDescription": "As permissões foram atualizadas.", - "apiKeysPermissionsGeneralSettings": "Permissões", - "apiKeysPermissionsGeneralSettingsDescription": "Determine o que esta chave API pode fazer", - "apiKeysPermissionsSave": "Guardar Permissões", - "apiKeysPermissionsTitle": "Permissões", - "apiKeys": "Chaves API", - "searchApiKeys": "Pesquisar chaves API...", - "apiKeysAdd": "Gerar 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": "Gerir Chaves API", - "apiKeysDescription": "As chaves API são usadas para autenticar com a API de integração", - "apiKeysSettings": "Configurações de {apiKeyName}", - "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 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 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", - "licenseKeySearch": "Pesquisar chaves da licença...", - "licenseKeyAdd": "Adicionar chave de licença", - "type": "tipo", - "licenseKeyRequired": "A chave da licença é necessária", - "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 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", - "licenseErrorKeyActivateDescription": "Ocorreu um erro ao ativar a chave da licença.", - "licenseAbout": "Sobre Licenciamento", - "communityEdition": "Edição da Comunidade", - "licenseAboutDescription": "Isto destina-se aos utilizadores empresariais e empresariais que estão a usar o Pangolin num ambiente comercial. Se você estiver usando o Pangolin para uso pessoal, você pode ignorar esta seção.", - "licenseKeyActivated": "Chave de licença ativada", - "licenseKeyActivatedDescription": "A chave de licença foi ativada com sucesso.", - "licenseErrorKeyRecheck": "Falha ao verificar novamente as chaves de licença", - "licenseErrorKeyRecheckDescription": "Ocorreu um erro ao reverificar a chave de licença.", - "licenseErrorKeyRechecked": "Chaves de licença reverificadas", - "licenseErrorKeyRecheckedDescription": "Todas as chaves de licença foram remarcadas", - "licenseActivateKey": "Ativar Chave de Licença", - "licenseActivateKeyDescription": "Insira uma chave de licença para ativá-la.", - "licenseActivate": "Ativar Licença", - "licenseAgreement": "Ao marcar esta caixa, você confirma que leu e concorda com os termos de licença correspondentes ao nível associado à sua chave de licença.", - "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 apagar a chave de licença {selectedKey}?", - "licenseKeyDelete": "Excluir Chave de Licença", - "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": "Gerir a chave de licença principal do host.", - "licensedNot": "Não Licenciado", - "hostId": "ID do host", - "licenseReckeckAll": "Verifique novamente todas as chaves", - "licenseSiteUsage": "Uso de Sites", - "licenseSiteUsageDecsription": "Exibir o número de sites utilizando esta licença.", - "licenseNoSiteLimit": "Não há limite para o número de sites utilizando um host não licenciado.", - "licensePurchase": "Comprar Licença", - "licensePurchaseSites": "Comprar Sites Adicionais", - "licenseSitesUsedMax": "{usedSites} de {maxSites} utilizados", - "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} no sistema.", - "licensePurchaseDescription": "Escolha quantos sites você quer {selectedMode, select, license {Compre uma licença. Você sempre pode adicionar mais sites depois.} other {adicione à sua licença existente.}}", - "licenseFee": "Taxa de licença", - "licensePriceSite": "Preço por site", - "total": "Total:", - "licenseContinuePayment": "Continuar para o pagamento", - "pricingPage": "Página de preços", - "pricingPortal": "Ver Portal de Compra", - "licensePricingPage": "Para os preços e descontos mais atualizados, por favor, visite ", - "invite": "Convites", - "inviteRegenerate": "Regenerar Convite", - "inviteRegenerateDescription": "Revogar convite anterior e criar um novo", - "inviteRemove": "Remover Convite", - "inviteRemoveError": "Falha ao remover convite", - "inviteRemoveErrorDescription": "Ocorreu um erro ao remover o convite.", - "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 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 utilizador", - "inviteGenerate": "Um novo convite foi gerado para {email}.", - "inviteDuplicateError": "Convite Duplicado", - "inviteDuplicateErrorDescription": "Já existe um convite para este utilizador.", - "inviteRateLimitError": "Limite de Taxa Excedido", - "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 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 utilizador", - "userErrorOrgRemoveDescription": "Ocorreu um erro ao remover o utilizador.", - "userOrgRemoved": "Usuário removido", - "userOrgRemovedDescription": "O utilizador {email} foi removido da organização.", - "userQuestionOrgRemove": "Tem certeza que deseja remover {email} da organização?", - "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": "Utilizadores", - "accessRoleMember": "Membro", - "accessRoleOwner": "Proprietário", - "userConfirmed": "Confirmado", - "idpNameInternal": "Interno", - "emailInvalid": "Endereço de email inválido", - "inviteValidityDuration": "Por favor, selecione uma duração", - "accessRoleSelectPlease": "Por favor, selecione uma função", - "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", - "accessRoleErrorFetchDescription": "Ocorreu um erro ao buscar as funções", - "idpErrorFetch": "Falha ao buscar provedores de identidade", - "idpErrorFetchDescription": "Ocorreu um erro ao buscar provedores de identidade", - "userErrorExists": "Usuário já existe", - "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 utilizador foi convidado com sucesso.", - "userErrorCreate": "Falha ao criar utilizador", - "userErrorCreateDescription": "Ocorreu um erro ao criar o utilizador", - "userCreated": "Usuário criado", - "userCreatedDescription": "O utilizador foi criado com sucesso.", - "userTypeInternal": "Usuário Interno", - "userTypeInternalDescription": "Convidar um utilizador para se juntar à sua organização diretamente.", - "userTypeExternal": "Usuário Externo", - "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 utilizador", - "userSettings": "Informações do 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 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 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": "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 utilizador foi atualizado.", - "autoProvisioned": "Auto provisionado", - "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": "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", - "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", - "siteErrorFetchDescription": "Ocorreu um erro ao buscar recurso", - "targetErrorDuplicate": "Alvo duplicado", - "targetErrorDuplicateDescription": "Um alvo com estas configurações já existe", - "targetWireGuardErrorInvalidIp": "IP do alvo inválido", - "targetWireGuardErrorInvalidIpDescription": "O IP do alvo deve estar dentro da subnet do site", - "targetsUpdated": "Alvos atualizados", - "targetsUpdatedDescription": "Alvos e configurações atualizados com sucesso", - "targetsErrorUpdate": "Falha ao atualizar alvos", - "targetsErrorUpdateDescription": "Ocorreu um erro ao atualizar alvos", - "targetTlsUpdate": "Configurações TLS atualizadas", - "targetTlsUpdateDescription": "Suas configurações TLS foram atualizadas com sucesso", - "targetErrorTlsUpdate": "Falha ao atualizar configurações TLS", - "targetErrorTlsUpdateDescription": "Ocorreu um erro ao atualizar as configurações TLS", - "proxyUpdated": "Configurações de proxy atualizadas", - "proxyUpdatedDescription": "Suas configurações de proxy foram atualizadas com sucesso", - "proxyErrorUpdate": "Falha ao atualizar configurações de proxy", - "proxyErrorUpdateDescription": "Ocorreu um erro ao atualizar as configurações de proxy", - "targetAddr": "IP / Nome do Host", - "targetPort": "Porta", - "targetProtocol": "Protocolo", - "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", - "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", - "targetTlsSubmit": "Guardar Configurações", - "targets": "Configuração de Alvos", - "targetsDescription": "Configure alvos para rotear tráfego para seus serviços de backend", - "targetStickySessions": "Ativar Sessões Persistentes", - "targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.", - "methodSelect": "Selecionar método", - "targetSubmit": "Adicionar Alvo", - "targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.", - "targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.", - "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": "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", - "path": "Caminho", - "matchPath": "Correspondência de caminho", - "ipAddressRange": "Faixa de IP", - "rulesErrorFetch": "Falha ao buscar regras", - "rulesErrorFetchDescription": "Ocorreu um erro ao buscar regras", - "rulesErrorDuplicate": "Regra duplicada", - "rulesErrorDuplicateDescription": "Uma regra com estas configurações já existe", - "rulesErrorInvalidIpAddressRange": "CIDR inválido", - "rulesErrorInvalidIpAddressRangeDescription": "Por favor, insira um valor CIDR válido", - "rulesErrorInvalidUrl": "Caminho URL inválido", - "rulesErrorInvalidUrlDescription": "Por favor, insira um valor de caminho URL válido", - "rulesErrorInvalidIpAddress": "IP inválido", - "rulesErrorInvalidIpAddressDescription": "Por favor, insira um endereço IP válido", - "rulesErrorUpdate": "Falha ao atualizar regras", - "rulesErrorUpdateDescription": "Ocorreu um erro ao atualizar regras", - "rulesUpdated": "Ativar Regras", - "rulesUpdatedDescription": "A avaliação de regras foi atualizada", - "rulesMatchIpAddressRangeDescription": "Insira um endereço no formato CIDR (ex: 103.21.244.0/22)", - "rulesMatchIpAddress": "Insira um endereço IP (ex: 103.21.244.12)", - "rulesMatchUrl": "Insira um caminho URL ou padrão (ex: /api/v1/todos ou /api/v1/*)", - "rulesErrorInvalidPriority": "Prioridade Inválida", - "rulesErrorInvalidPriorityDescription": "Por favor, insira uma prioridade válida", - "rulesErrorDuplicatePriority": "Prioridades Duplicadas", - "rulesErrorDuplicatePriorityDescription": "Por favor, insira prioridades únicas", - "ruleUpdated": "Regras atualizadas", - "ruleUpdatedDescription": "Regras atualizadas com sucesso", - "ruleErrorUpdate": "Operação falhou", - "ruleErrorUpdateDescription": "Ocorreu um erro durante a operação de salvamento", - "rulesPriority": "Prioridade", - "rulesAction": "Ação", - "rulesMatchType": "Tipo de Correspondência", - "value": "Valor", - "rulesAbout": "Sobre Regras", - "rulesAboutDescription": "As regras permitem controlar o acesso ao seu recurso com base em um conjunto de critérios. Você pode criar regras para permitir ou negar acesso com base no endereço IP ou caminho URL.", - "rulesActions": "Ações", - "rulesActionAlwaysAllow": "Sempre Permitir: Ignorar todos os métodos de autenticação", - "rulesActionAlwaysDeny": "Sempre Negar: Bloquear todas as requisições; nenhuma autenticação pode ser tentada", - "rulesActionPassToAuth": "Passar para Autenticação: Permitir que métodos de autenticação sejam tentados", - "rulesMatchCriteria": "Critérios de Correspondência", - "rulesMatchCriteriaIpAddress": "Corresponder a um endereço IP específico", - "rulesMatchCriteriaIpAddressRange": "Corresponder a uma faixa de endereços IP em notação CIDR", - "rulesMatchCriteriaUrl": "Corresponder a um caminho URL ou padrão", - "rulesEnable": "Ativar Regras", - "rulesEnableDescription": "Ativar ou desativar avaliação de regras para este recurso", - "rulesResource": "Configuração de Regras do Recurso", - "rulesResourceDescription": "Configure regras para controlar o acesso ao seu recurso", - "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": "Guardar Regras", - "resourceErrorCreate": "Erro ao criar recurso", - "resourceErrorCreateDescription": "Ocorreu um erro ao criar o recurso", - "resourceErrorCreateMessage": "Erro ao criar recurso:", - "resourceErrorCreateMessageDescription": "Ocorreu um erro inesperado", - "sitesErrorFetch": "Erro ao buscar sites", - "sitesErrorFetchDescription": "Ocorreu um erro ao buscar os sites", - "domainsErrorFetch": "Erro ao buscar domínios", - "domainsErrorFetchDescription": "Ocorreu um erro ao buscar os domínios", - "none": "Nenhum", - "unknown": "Desconhecido", - "resources": "Recursos", - "resourcesDescription": "Recursos são proxies para aplicações executando em sua rede privada. Crie um recurso para qualquer serviço HTTP/HTTPS ou TCP/UDP bruto em sua rede privada. Cada recurso deve estar conectado a um site para habilitar conectividade privada e segura através de um túnel WireGuard criptografado.", - "resourcesWireGuardConnect": "Conectividade segura com criptografia WireGuard", - "resourcesMultipleAuthenticationMethods": "Configure múltiplos métodos de autenticação", - "resourcesUsersRolesAccess": "Controle de acesso baseado em utilizadores e funções", - "resourcesErrorUpdate": "Falha ao alternar recurso", - "resourcesErrorUpdateDescription": "Ocorreu um erro ao atualizar o recurso", - "access": "Acesso", - "shareLink": "Link de Compartilhamento {resource}", - "resourceSelect": "Selecionar recurso", - "shareLinks": "Links de Compartilhamento", - "share": "Links Compartilháveis", - "shareDescription2": "Crie links compartilháveis para seus recursos. Os links fornecem acesso temporário ou ilimitado ao seu recurso. Você pode configurar a duração da expiração do link quando o criar.", - "shareEasyCreate": "Fácil de criar e compartilhar", - "shareConfigurableExpirationDuration": "Duração de expiração configurável", - "shareSecureAndRevocable": "Seguro e revogável", - "nameMin": "O nome deve ter pelo menos {len} caracteres.", - "nameMax": "O nome não deve ter mais de {len} caracteres.", - "sitesConfirmCopy": "Por favor, confirme que você copiou a configuração.", - "unknownCommand": "Comando desconhecido", - "newtErrorFetchReleases": "Falha ao buscar informações da versão: {err}", - "newtErrorFetchLatest": "Erro ao buscar última versão: {err}", - "newtEndpoint": "Endpoint Newt", - "newtId": "ID Newt", - "newtSecretKey": "Chave Secreta Newt", - "architecture": "Arquitetura", - "sites": "sites", - "siteWgAnyClients": "Use qualquer cliente WireGuard para conectar. Você terá que endereçar seus recursos internos usando o IP do par.", - "siteWgCompatibleAllClients": "Compatível com todos os clientes WireGuard", - "siteWgManualConfigurationRequired": "Configuração manual necessária", - "userErrorNotAdminOrOwner": "Usuário não é administrador ou proprietário", - "pangolinSettings": "Configurações - Pangolin", - "accessRoleYour": "Sua função:", - "accessRoleSelect2": "Selecionar uma função", - "accessUserSelect": "Selecionar um utilizador", - "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.", - "otpEmailSmtpRequired": "SMTP Necessário", - "otpEmailSmtpRequiredDescription": "O SMTP deve estar habilitado no servidor para usar a autenticação de senha única.", - "otpEmailTitle": "Senhas Únicas", - "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 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", - "pincodeRemove": "Remover Código PIN", - "resourceAuthMethods": "Métodos de Autenticação", - "resourceAuthMethodsDescriptions": "Permitir acesso ao recurso via métodos de autenticação adicionais", - "resourceAuthSettingsSave": "Salvo com sucesso", - "resourceAuthSettingsSaveDescription": "As configurações de autenticação foram salvas", - "resourceErrorAuthFetch": "Falha ao buscar dados", - "resourceErrorAuthFetchDescription": "Ocorreu um erro ao buscar os dados", - "resourceErrorPasswordRemove": "Erro ao remover senha do recurso", - "resourceErrorPasswordRemoveDescription": "Ocorreu um erro ao remover a senha do recurso", - "resourceErrorPasswordSetup": "Erro ao definir senha do recurso", - "resourceErrorPasswordSetupDescription": "Ocorreu um erro ao definir a senha do recurso", - "resourceErrorPincodeRemove": "Erro ao remover código PIN do recurso", - "resourceErrorPincodeRemoveDescription": "Ocorreu um erro ao remover o código PIN do recurso", - "resourceErrorPincodeSetup": "Erro ao definir código PIN do recurso", - "resourceErrorPincodeSetupDescription": "Ocorreu um erro ao definir o código PIN do recurso", - "resourceErrorUsersRolesSave": "Falha ao definir funções", - "resourceErrorUsersRolesSaveDescription": "Ocorreu um erro ao definir as funções", - "resourceErrorWhitelistSave": "Falha ao salvar lista permitida", - "resourceErrorWhitelistSaveDescription": "Ocorreu um erro ao salvar a lista permitida", - "resourcePasswordSubmit": "Habilitar Proteção por Senha", - "resourcePasswordProtection": "Proteção com senha {status}", - "resourcePasswordRemove": "Senha do recurso removida", - "resourcePasswordRemoveDescription": "A senha do recurso foi removida com sucesso", - "resourcePasswordSetup": "Senha do recurso definida", - "resourcePasswordSetupDescription": "A senha do recurso foi definida com sucesso", - "resourcePasswordSetupTitle": "Definir Senha", - "resourcePasswordSetupTitleDescription": "Defina uma senha para proteger este recurso", - "resourcePincode": "Código PIN", - "resourcePincodeSubmit": "Habilitar Proteção por Código PIN", - "resourcePincodeProtection": "Proteção por Código PIN {status}", - "resourcePincodeRemove": "Código PIN do recurso removido", - "resourcePincodeRemoveDescription": "O código PIN do recurso foi removido com sucesso", - "resourcePincodeSetup": "Código PIN do recurso definido", - "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 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 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", - "domainErrorFetchDescription": "Ocorreu um erro ao buscar os domínios", - "resourceErrorUpdate": "Falha ao atualizar recurso", - "resourceErrorUpdateDescription": "Ocorreu um erro ao atualizar o recurso", - "resourceUpdated": "Recurso atualizado", - "resourceUpdatedDescription": "O recurso foi atualizado com sucesso", - "resourceErrorTransfer": "Falha ao transferir recurso", - "resourceErrorTransferDescription": "Ocorreu um erro ao transferir o recurso", - "resourceTransferred": "Recurso transferido", - "resourceTransferredDescription": "O recurso foi transferido com sucesso", - "resourceErrorToggle": "Falha ao alternar recurso", - "resourceErrorToggleDescription": "Ocorreu um erro ao atualizar o recurso", - "resourceVisibilityTitle": "Visibilidade", - "resourceVisibilityTitleDescription": "Ativar ou desativar completamente a visibilidade do recurso", - "resourceGeneral": "Configurações Gerais", - "resourceGeneralDescription": "Configure as configurações gerais para este recurso", - "resourceEnable": "Ativar Recurso", - "resourceTransfer": "Transferir Recurso", - "resourceTransferDescription": "Transferir este recurso para um site diferente", - "resourceTransferSubmit": "Transferir Recurso", - "siteDestination": "Site de Destino", - "searchSites": "Pesquisar sites", - "accessRoleCreate": "Criar Função", - "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.", - "accessRoleErrorCreate": "Falha ao criar função", - "accessRoleErrorCreateDescription": "Ocorreu um erro ao criar a função.", - "accessRoleErrorNewRequired": "Nova função é necessária", - "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 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 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", - "licenseTierPersonal": "Personal License", - "licensed": "Licenciado", - "yes": "Sim", - "no": "Não", - "sitesAdditional": "Sites Adicionais", - "licenseKeys": "Chaves de Licença", - "sitestCountDecrease": "Diminuir contagem de sites", - "sitestCountIncrease": "Aumentar contagem de sites", - "idpManage": "Gerir Provedores de Identidade", - "idpManageDescription": "Visualizar e gerir provedores de identidade no sistema", - "idpDeletedDescription": "Provedor de identidade eliminado com sucesso", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Tem certeza que deseja eliminar permanentemente o provedor de identidade {name}?", - "idpMessageRemove": "Isto irá remover o provedor de identidade e todas as configurações associadas. Os utilizadores que se autenticam através deste provedor não poderão mais fazer login.", - "idpMessageConfirm": "Para confirmar, por favor digite o nome do provedor de identidade abaixo.", - "idpConfirmDelete": "Confirmar Eliminação do Provedor de Identidade", - "idpDelete": "Eliminar Provedor de Identidade", - "idp": "Provedores de Identidade", - "idpSearch": "Pesquisar provedores de identidade...", - "idpAdd": "Adicionar Provedor de Identidade", - "idpClientIdRequired": "O ID do Cliente é obrigatório.", - "idpClientSecretRequired": "O Segredo do Cliente é obrigatório.", - "idpErrorAuthUrlInvalid": "O URL de Autenticação deve ser um URL válido.", - "idpErrorTokenUrlInvalid": "O URL do Token deve ser um URL válido.", - "idpPathRequired": "O Caminho do Identificador é obrigatório.", - "idpScopeRequired": "Os Escopos são obrigatórios.", - "idpOidcDescription": "Configurar um provedor de identidade OpenID Connect", - "idpCreatedDescription": "Provedor de identidade criado com sucesso", - "idpCreate": "Criar Provedor de Identidade", - "idpCreateDescription": "Configurar um novo provedor de identidade para autenticação de utilizadores", - "idpSeeAll": "Ver Todos os Provedores de Identidade", - "idpSettingsDescription": "Configurar as informações básicas para o seu provedor de identidade", - "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": "EE", - "idpType": "Tipo de Provedor", - "idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar", - "idpOidcConfigure": "Configuração OAuth2/OIDC", - "idpOidcConfigureDescription": "Configurar os endpoints e credenciais do provedor OAuth2/OIDC", - "idpClientId": "ID do Cliente", - "idpClientIdDescription": "O ID do cliente OAuth2 do seu provedor de identidade", - "idpClientSecret": "Segredo do Cliente", - "idpClientSecretDescription": "O segredo do cliente OAuth2 do seu provedor de identidade", - "idpAuthUrl": "URL de Autorização", - "idpAuthUrlDescription": "O URL do endpoint de autorização OAuth2", - "idpTokenUrl": "URL do Token", - "idpTokenUrlDescription": "O URL do endpoint do token OAuth2", - "idpOidcConfigureAlert": "Informação Importante", - "idpOidcConfigureAlertDescription": "Após criar o provedor de identidade, será necessário configurar o URL de retorno nas configurações do seu provedor de identidade. O URL de retorno será fornecido após a criação bem-sucedida.", - "idpToken": "Configuração do Token", - "idpTokenDescription": "Configurar como extrair informações do utilizador do token ID", - "idpJmespathAbout": "Sobre JMESPath", - "idpJmespathAboutDescription": "Os caminhos abaixo usam a sintaxe JMESPath para extrair valores do token ID.", - "idpJmespathAboutDescriptionLink": "Saiba mais sobre JMESPath", - "idpJmespathLabel": "Caminho do Identificador", - "idpJmespathLabelDescription": "O JMESPath para o identificador do utilizador no token ID", - "idpJmespathEmailPathOptional": "Caminho do Email (Opcional)", - "idpJmespathEmailPathOptionalDescription": "O JMESPath para o email do utilizador no token ID", - "idpJmespathNamePathOptional": "Caminho do Nome (Opcional)", - "idpJmespathNamePathOptionalDescription": "O JMESPath para o nome do utilizador no token ID", - "idpOidcConfigureScopes": "Escopos", - "idpOidcConfigureScopesDescription": "Lista de escopos OAuth2 separados por espaço para solicitar", - "idpSubmit": "Criar Provedor de Identidade", - "orgPolicies": "Políticas da Organização", - "idpSettings": "Configurações de {idpName}", - "idpCreateSettingsDescription": "Configurar as definições para o seu provedor de identidade", - "roleMapping": "Mapeamento de Funções", - "orgMapping": "Mapeamento da Organização", - "orgPoliciesSearch": "Pesquisar políticas da organização...", - "orgPoliciesAdd": "Adicionar Política da Organização", - "orgRequired": "A organização é obrigatória", - "error": "Erro", - "success": "Sucesso", - "orgPolicyAddedDescription": "Política adicionada com sucesso", - "orgPolicyUpdatedDescription": "Política atualizada com sucesso", - "orgPolicyDeletedDescription": "Política eliminada com sucesso", - "defaultMappingsUpdatedDescription": "Mapeamentos padrão atualizados com sucesso", - "orgPoliciesAbout": "Sobre Políticas da Organização", - "orgPoliciesAboutDescription": "As políticas da organização são usadas para controlar o acesso às organizações com base no token ID do utilizador. Pode especificar expressões JMESPath para extrair informações de função e organização do token ID. Para mais informações, consulte", - "orgPoliciesAboutDescriptionLink": "a documentação", - "defaultMappingsOptional": "Mapeamentos Padrão (Opcional)", - "defaultMappingsOptionalDescription": "Os mapeamentos padrão são usados quando não há uma política de organização definida para uma organização. Pode especificar aqui os mapeamentos padrão de função e organização para recorrer.", - "defaultMappingsRole": "Mapeamento de Função Padrão", - "defaultMappingsRoleDescription": "JMESPath para extrair informações de função do token ID. O resultado desta expressão deve retornar o nome da função como definido na organização como uma string.", - "defaultMappingsOrg": "Mapeamento de Organização Padrão", - "defaultMappingsOrgDescription": "JMESPath para extrair informações da organização do token ID. Esta expressão deve retornar o ID da organização ou verdadeiro para que o utilizador tenha permissão para aceder à organização.", - "defaultMappingsSubmit": "Guardar Mapeamentos Padrão", - "orgPoliciesEdit": "Editar Política da Organização", - "org": "Organização", - "orgSelect": "Selecionar organização", - "orgSearch": "Pesquisar organização", - "orgNotFound": "Nenhuma organização encontrada.", - "roleMappingPathOptional": "Caminho de Mapeamento de Função (Opcional)", - "orgMappingPathOptional": "Caminho de Mapeamento da Organização (Opcional)", - "orgPolicyUpdate": "Atualizar Política", - "orgPolicyAdd": "Adicionar Política", - "orgPolicyConfig": "Configurar acesso para uma organização", - "idpUpdatedDescription": "Provedor de identidade atualizado com sucesso", - "redirectUrl": "URL de Redirecionamento", - "redirectUrlAbout": "Sobre o URL de Redirecionamento", - "redirectUrlAboutDescription": "Este é o URL para o qual os utilizadores serão redirecionados após a autenticação. Precisa configurar este URL nas configurações do seu provedor de identidade.", - "pangolinAuth": "Autenticação - Pangolin", - "verificationCodeLengthRequirements": "O seu código de verificação deve ter 8 caracteres.", - "errorOccurred": "Ocorreu um erro", - "emailErrorVerify": "Falha ao verificar o email:", - "emailVerified": "Email verificado com sucesso! Redirecionando...", - "verificationCodeErrorResend": "Falha ao reenviar o código de verificação:", - "verificationCodeResend": "Código de verificação reenviado", - "verificationCodeResendDescription": "Reenviámos um código de verificação para o seu email. Por favor, verifique a sua caixa de entrada.", - "emailVerify": "Verificar Email", - "emailVerifyDescription": "Insira o código de verificação enviado para o seu email.", - "verificationCode": "Código de Verificação", - "verificationCodeEmailSent": "Enviámos um código de verificação para o seu email.", - "submit": "Submeter", - "emailVerifyResendProgress": "A reenviar...", - "emailVerifyResend": "Não recebeu um código? Clique aqui para reenviar", - "passwordNotMatch": "As palavras-passe não correspondem", - "signupError": "Ocorreu um erro durante o registo", - "pangolinLogoAlt": "Logótipo Pangolin", - "inviteAlready": "Parece que já foi convidado!", - "inviteAlreadyDescription": "Para aceitar o convite, deve iniciar sessão ou criar uma conta.", - "signupQuestion": "Já tem uma conta?", - "login": "Iniciar sessão", - "resourceNotFound": "Recurso Não Encontrado", - "resourceNotFoundDescription": "O recurso que está a tentar aceder não existe.", - "pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos", - "pincodeRequirementsChars": "O PIN deve conter apenas números", - "passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere", - "passwordRequirementsTitle": "Requisitos de senha:", - "passwordRequirementLength": "Pelo menos 8 caracteres de comprimento", - "passwordRequirementUppercase": "Pelo menos uma letra maiúscula", - "passwordRequirementLowercase": "Pelo menos uma letra minúscula", - "passwordRequirementNumber": "Pelo menos um número", - "passwordRequirementSpecial": "Pelo menos um caractere especial", - "passwordRequirementsMet": "✓ Senha atende a todos os requisitos", - "passwordStrength": "Força da senha", - "passwordStrengthWeak": "Fraca", - "passwordStrengthMedium": "Média", - "passwordStrengthStrong": "Forte", - "passwordRequirements": "Requisitos:", - "passwordRequirementLengthText": "8+ caracteres", - "passwordRequirementUppercaseText": "Letra maiúscula (A-Z)", - "passwordRequirementLowercaseText": "Letra minúscula (a-z)", - "passwordRequirementNumberText": "Número (0-9)", - "passwordRequirementSpecialText": "Caractere especial (!@#$%...)", - "passwordsDoNotMatch": "As palavras-passe não correspondem", - "otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere", - "otpEmailSent": "OTP Enviado", - "otpEmailSentDescription": "Um OTP foi enviado para o seu email", - "otpEmailErrorAuthenticate": "Falha na autenticação por email", - "pincodeErrorAuthenticate": "Falha na autenticação com PIN", - "passwordErrorAuthenticate": "Falha na autenticação com palavra-passe", - "poweredBy": "Desenvolvido por", - "authenticationRequired": "Autenticação Necessária", - "authenticationMethodChoose": "Escolha o seu método preferido para aceder a {name}", - "authenticationRequest": "Deve autenticar-se para aceder a {name}", - "user": "Utilizador", - "pincodeInput": "Código PIN de 6 dígitos", - "pincodeSubmit": "Iniciar sessão com PIN", - "passwordSubmit": "Iniciar Sessão com Palavra-passe", - "otpEmailDescription": "Um código único será enviado para este email.", - "otpEmailSend": "Enviar Código Único", - "otpEmail": "Palavra-passe Única (OTP)", - "otpEmailSubmit": "Submeter OTP", - "backToEmail": "Voltar ao Email", - "noSupportKey": "O servidor está rodando sem uma chave de suporte. Considere apoiar o projeto!", - "accessDenied": "Acesso Negado", - "accessDeniedDescription": "Não tem permissão para aceder a este recurso. Se isto for um erro, contacte o administrador.", - "accessTokenError": "Erro ao verificar o token de acesso", - "accessGranted": "Acesso Concedido", - "accessUrlInvalid": "URL de Acesso Inválido", - "accessGrantedDescription": "Foi-lhe concedido acesso a este recurso. A redirecionar...", - "accessUrlInvalidDescription": "Este URL de acesso partilhado é inválido. Por favor, contacte o proprietário do recurso para obter um novo URL.", - "tokenInvalid": "Token inválido", - "pincodeInvalid": "Código inválido", - "passwordErrorRequestReset": "Falha ao solicitar redefinição:", - "passwordErrorReset": "Falha ao redefinir palavra-passe:", - "passwordResetSuccess": "Palavra-passe redefinida com sucesso! Voltar ao início de sessão...", - "passwordReset": "Redefinir Palavra-passe", - "passwordResetDescription": "Siga os passos para redefinir a sua palavra-passe", - "passwordResetSent": "Enviaremos um código de redefinição de palavra-passe para este email.", - "passwordResetCode": "Código de Redefinição", - "passwordResetCodeDescription": "Verifique o seu email para obter o código de redefinição.", - "passwordNew": "Nova Palavra-passe", - "passwordNewConfirm": "Confirmar Nova Palavra-passe", - "pincodeAuth": "Código do Autenticador", - "pincodeSubmit2": "Submeter Código", - "passwordResetSubmit": "Solicitar Redefinição", - "passwordBack": "Voltar à Palavra-passe", - "loginBack": "Voltar ao início de sessão", - "signup": "Registar", - "loginStart": "Inicie sessão para começar", - "idpOidcTokenValidating": "A validar token OIDC", - "idpOidcTokenResponse": "Validar resposta do token OIDC", - "idpErrorOidcTokenValidating": "Erro ao validar token OIDC", - "idpConnectingTo": "A ligar a {name}", - "idpConnectingToDescription": "A validar a sua identidade", - "idpConnectingToProcess": "A conectar...", - "idpConnectingToFinished": "Conectado", - "idpErrorConnectingTo": "Ocorreu um problema ao ligar a {name}. Por favor, contacte o seu administrador.", - "idpErrorNotFound": "IdP não encontrado", - "inviteInvalid": "Convite Inválido", - "inviteInvalidDescription": "O link do convite é inválido.", - "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", - "inviteErrorTypo": "Pode haver um erro de digitação no link do convite", - "pangolinSetup": "Configuração - Pangolin", - "orgNameRequired": "O nome da organização é obrigatório", - "orgIdRequired": "O ID da organização é obrigatório", - "orgErrorCreate": "Ocorreu um erro ao criar a organização", - "pageNotFound": "Página Não Encontrada", - "pageNotFoundDescription": "Ops! A página que você está procurando não existe.", - "overview": "Visão Geral", - "home": "Início", - "accessControl": "Controle de Acesso", - "settings": "Configurações", - "usersAll": "Todos os Utilizadores", - "license": "Licença", - "pangolinDashboard": "Painel - Pangolin", - "noResults": "Nenhum resultado encontrado.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Tags Inseridas", - "tagsEnteredDescription": "Estas são as tags que você inseriu.", - "tagsWarnCannotBeLessThanZero": "maxTags e minTags não podem ser menores que 0", - "tagsWarnNotAllowedAutocompleteOptions": "Tag não permitida conforme as opções de autocompletar", - "tagsWarnInvalid": "Tag inválida conforme validateTag", - "tagWarnTooShort": "A tag {tagText} é muito curta", - "tagWarnTooLong": "A tag {tagText} é muito longa", - "tagsWarnReachedMaxNumber": "Atingido o número máximo de tags permitidas", - "tagWarnDuplicate": "Tag duplicada {tagText} não adicionada", - "supportKeyInvalid": "Chave Inválida", - "supportKeyInvalidDescription": "A sua chave de suporte é inválida.", - "supportKeyValid": "Chave Válida", - "supportKeyValidDescription": "A sua chave de suporte foi validada. Obrigado pelo seu apoio!", - "supportKeyErrorValidationDescription": "Falha ao validar a chave de suporte.", - "supportKey": "Apoie o Desenvolvimento e Adote um Pangolim!", - "supportKeyDescription": "Compre uma chave de suporte para nos ajudar a continuar desenvolvendo o Pangolin para a comunidade. A sua contribuição permite-nos dedicar mais tempo para manter e adicionar novos recursos à aplicação para todos. Nunca usaremos isto para restringir recursos. Isto é separado de qualquer Edição Comercial.", - "supportKeyPet": "Também poderá adotar e conhecer o seu próprio Pangolim de estimação!", - "supportKeyPurchase": "Os pagamentos são processados via GitHub. Depois, pode obter a sua chave em", - "supportKeyPurchaseLink": "nosso site", - "supportKeyPurchase2": "e resgatá-la aqui.", - "supportKeyLearnMore": "Saiba mais.", - "supportKeyOptions": "Por favor, selecione a opção que melhor se adequa a si.", - "supportKetOptionFull": "Apoiante Completo", - "forWholeServer": "Para todo o servidor", - "lifetimePurchase": "Compra vitalícia", - "supporterStatus": "Estado de apoiante", - "buy": "Comprar", - "supportKeyOptionLimited": "Apoiante Limitado", - "forFiveUsers": "Para 5 ou menos utilizadores", - "supportKeyRedeem": "Resgatar Chave de Apoiante", - "supportKeyHideSevenDays": "Ocultar por 7 dias", - "supportKeyEnter": "Inserir Chave de Apoiante", - "supportKeyEnterDescription": "Conheça o seu próprio Pangolim de estimação!", - "githubUsername": "Nome de Utilizador GitHub", - "supportKeyInput": "Chave de Apoiante", - "supportKeyBuy": "Comprar Chave de Apoiante", - "logoutError": "Erro ao terminar sessão", - "signingAs": "Sessão iniciada como", - "serverAdmin": "Administrador do Servidor", - "managedSelfhosted": "Gerenciado Auto-Hospedado", - "otpEnable": "Ativar Autenticação de Dois Fatores", - "otpDisable": "Desativar Autenticação de Dois Fatores", - "logout": "Terminar Sessão", - "licenseTierProfessionalRequired": "Edição Profissional Necessária", - "licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.", - "actionGetOrg": "Obter Organização", - "updateOrgUser": "Atualizar utilizador Org", - "createOrgUser": "Criar utilizador Org", - "actionUpdateOrg": "Atualizar Organização", - "actionUpdateUser": "Atualizar Usuário", - "actionGetUser": "Obter Usuário", - "actionGetOrgUser": "Obter Utilizador da Organização", - "actionListOrgDomains": "Listar Domínios da Organização", - "actionCreateSite": "Criar Site", - "actionDeleteSite": "Eliminar Site", - "actionGetSite": "Obter Site", - "actionListSites": "Listar Sites", - "actionApplyBlueprint": "Aplicar Diagrama", - "setupToken": "Configuração do Token", - "setupTokenDescription": "Digite o token de configuração do console do servidor.", - "setupTokenRequired": "Token de configuração é necessário", - "actionUpdateSite": "Atualizar Site", - "actionListSiteRoles": "Listar Funções Permitidas do Site", - "actionCreateResource": "Criar Recurso", - "actionDeleteResource": "Eliminar Recurso", - "actionGetResource": "Obter Recurso", - "actionListResource": "Listar Recursos", - "actionUpdateResource": "Atualizar Recurso", - "actionListResourceUsers": "Listar Utilizadores do Recurso", - "actionSetResourceUsers": "Definir Utilizadores do Recurso", - "actionSetAllowedResourceRoles": "Definir Funções Permitidas do Recurso", - "actionListAllowedResourceRoles": "Listar Funções Permitidas do Recurso", - "actionSetResourcePassword": "Definir Palavra-passe do Recurso", - "actionSetResourcePincode": "Definir Código PIN do Recurso", - "actionSetResourceEmailWhitelist": "Definir Lista Permitida de Emails do Recurso", - "actionGetResourceEmailWhitelist": "Obter Lista Permitida de Emails do Recurso", - "actionCreateTarget": "Criar Alvo", - "actionDeleteTarget": "Eliminar Alvo", - "actionGetTarget": "Obter Alvo", - "actionListTargets": "Listar Alvos", - "actionUpdateTarget": "Atualizar Alvo", - "actionCreateRole": "Criar Função", - "actionDeleteRole": "Eliminar Função", - "actionGetRole": "Obter Função", - "actionListRole": "Listar Funções", - "actionUpdateRole": "Atualizar Função", - "actionListAllowedRoleResources": "Listar Recursos Permitidos da Função", - "actionInviteUser": "Convidar Utilizador", - "actionRemoveUser": "Remover Utilizador", - "actionListUsers": "Listar Utilizadores", - "actionAddUserRole": "Adicionar Função ao Utilizador", - "actionGenerateAccessToken": "Gerar Token de Acesso", - "actionDeleteAccessToken": "Eliminar Token de Acesso", - "actionListAccessTokens": "Listar Tokens de Acesso", - "actionCreateResourceRule": "Criar Regra de Recurso", - "actionDeleteResourceRule": "Eliminar Regra de Recurso", - "actionListResourceRules": "Listar Regras de Recurso", - "actionUpdateResourceRule": "Atualizar Regra de Recurso", - "actionListOrgs": "Listar Organizações", - "actionCheckOrgId": "Verificar ID", - "actionCreateOrg": "Criar Organização", - "actionDeleteOrg": "Eliminar Organização", - "actionListApiKeys": "Listar Chaves API", - "actionListApiKeyActions": "Listar Ações da Chave API", - "actionSetApiKeyActions": "Definir Ações Permitidas da Chave API", - "actionCreateApiKey": "Criar Chave API", - "actionDeleteApiKey": "Eliminar Chave API", - "actionCreateIdp": "Criar IDP", - "actionUpdateIdp": "Atualizar IDP", - "actionDeleteIdp": "Eliminar IDP", - "actionListIdps": "Listar IDP", - "actionGetIdp": "Obter IDP", - "actionCreateIdpOrg": "Criar Política de Organização IDP", - "actionDeleteIdpOrg": "Eliminar Política de Organização IDP", - "actionListIdpOrgs": "Listar Organizações IDP", - "actionUpdateIdpOrg": "Atualizar Organização IDP", - "actionCreateClient": "Criar Cliente", - "actionDeleteClient": "Excluir Cliente", - "actionUpdateClient": "Atualizar Cliente", - "actionListClients": "Listar Clientes", - "actionGetClient": "Obter Cliente", - "actionCreateSiteResource": "Criar Recurso do Site", - "actionDeleteSiteResource": "Eliminar Recurso do Site", - "actionGetSiteResource": "Obter Recurso do Site", - "actionListSiteResources": "Listar Recursos do Site", - "actionUpdateSiteResource": "Atualizar Recurso do Site", - "actionListInvitations": "Listar Convites", - "noneSelected": "Nenhum selecionado", - "orgNotFound2": "Nenhuma organização encontrada.", - "searchProgress": "Pesquisar...", - "create": "Criar", - "orgs": "Organizações", - "loginError": "Ocorreu um erro ao iniciar sessão", - "passwordForgot": "Esqueceu a sua palavra-passe?", - "otpAuth": "Autenticação de Dois Fatores", - "otpAuthDescription": "Insira o código da sua aplicação de autenticação ou um dos seus códigos de backup de uso único.", - "otpAuthSubmit": "Submeter Código", - "idpContinue": "Ou continuar com", - "otpAuthBack": "Voltar ao Início de Sessão", - "navbar": "Menu de Navegação", - "navbarDescription": "Menu de navegação principal da aplicação", - "navbarDocsLink": "Documentação", - "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", - "otpSetupCheckCodeRetry": "Código inválido. Por favor, tente novamente.", - "otpSetup": "Ativar Autenticação de Dois Fatores", - "otpSetupDescription": "Proteja a sua conta com uma camada extra de proteção", - "otpSetupScanQr": "Digitalize este código QR com a sua aplicação de autenticação ou insira a chave secreta manualmente:", - "otpSetupSecretCode": "Código de Autenticação", - "otpSetupSuccess": "Autenticação de Dois Fatores Ativada", - "otpSetupSuccessStoreBackupCodes": "A sua conta está agora mais segura. Não se esqueça de guardar os seus códigos de backup.", - "otpErrorDisable": "Não foi possível desativar 2FA", - "otpErrorDisableDescription": "Ocorreu um erro ao desativar 2FA", - "otpRemove": "Desativar Autenticação de Dois Fatores", - "otpRemoveDescription": "Desativar a autenticação de dois fatores para a sua conta", - "otpRemoveSuccess": "Autenticação de Dois Fatores Desativada", - "otpRemoveSuccessMessage": "A autenticação de dois fatores foi desativada para a sua conta. Pode ativá-la novamente a qualquer momento.", - "otpRemoveSubmit": "Desativar 2FA", - "paginator": "Página {current} de {last}", - "paginatorToFirst": "Ir para a primeira página", - "paginatorToPrevious": "Ir para a página anterior", - "paginatorToNext": "Ir para a próxima página", - "paginatorToLast": "Ir para a última página", - "copyText": "Copiar texto", - "copyTextFailed": "Falha ao copiar texto: ", - "copyTextClipboard": "Copiar para a área de transferência", - "inviteErrorInvalidConfirmation": "Confirmação inválida", - "passwordRequired": "A senha é obrigatória", - "allowAll": "Permitir todos", - "permissionsAllowAll": "Permitir Todas as Permissões", - "githubUsernameRequired": "O nome de utilizador GitHub é obrigatório", - "supportKeyRequired": "A chave de apoiante é obrigatória", - "passwordRequirementsChars": "A palavra-passe deve ter pelo menos 8 caracteres", - "language": "Idioma", - "verificationCodeRequired": "O código é obrigatório", - "userErrorNoUpdate": "Não existe utilizador para atualizar", - "siteErrorNoUpdate": "Não existe site para atualizar", - "resourceErrorNoUpdate": "Não existe recurso para atualizar", - "authErrorNoUpdate": "Não existem informações de autenticação para atualizar", - "orgErrorNoUpdate": "Não existe organização para atualizar", - "orgErrorNoProvided": "Nenhuma organização fornecida", - "apiKeysErrorNoUpdate": "Não existe chave API para atualizar", - "sidebarOverview": "Geral", - "sidebarHome": "Residencial", - "sidebarSites": "sites", - "sidebarResources": "Recursos", - "sidebarAccessControl": "Controle de Acesso", - "sidebarUsers": "Utilizadores", - "sidebarInvitations": "Convites", - "sidebarRoles": "Papéis", - "sidebarShareableLinks": "Links compartilháveis", - "sidebarApiKeys": "Chaves API", - "sidebarSettings": "Configurações", - "sidebarAllUsers": "Todos os utilizadores", - "sidebarIdentityProviders": "Provedores de identidade", - "sidebarLicense": "Tipo:", - "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.", - "enableDockerSocketLink": "Saiba mais", - "viewDockerContainers": "Ver contêineres Docker", - "containersIn": "Contêineres em {siteName}", - "selectContainerDescription": "Selecione qualquer contêiner para usar como hostname para este alvo. Clique em uma porta para usar uma porta.", - "containerName": "Nome:", - "containerImage": "Imagem:", - "containerState": "Estado:", - "containerNetworks": "Redes", - "containerHostnameIp": "Hostname/IP", - "containerLabels": "Marcadores", - "containerLabelsCount": "{count, plural, one {# rótulo} other {# rótulos}}", - "containerLabelsTitle": "Etiquetas do Contêiner", - "containerLabelEmpty": "", - "containerPorts": "Portas", - "containerPortsMore": "+ Mais{count}", - "containerActions": "Ações.", - "select": "Selecionar", - "noContainersMatchingFilters": "Nenhum contêiner encontrado corresponde aos filtros atuais.", - "showContainersWithoutPorts": "Mostrar contêineres sem portas", - "showStoppedContainers": "Mostrar contêineres parados", - "noContainersFound": "Nenhum contêiner encontrado. Certifique-se de que os contêineres Docker estão em execução.", - "searchContainersPlaceholder": "Pesquisar entre os contêineres {count}...", - "searchResultsCount": "{count, plural, one {# resultado} other {# resultados}}", - "filters": "Filtros", - "filterOptions": "Opções de Filtro", - "filterPorts": "Portas", - "filterStopped": "Parado", - "clearAllFilters": "Limpar todos os filtros", - "columns": "Colunas", - "toggleColumns": "Alternar Colunas", - "refreshContainersList": "Atualizar lista de contêineres", - "searching": "Buscando...", - "noContainersFoundMatching": "Nenhum recipiente encontrado \"{filter}\".", - "light": "claro", - "dark": "escuro", - "system": "sistema", - "theme": "Tema", - "subnetRequired": "Sub-rede é obrigatória", - "initialSetupTitle": "Configuração Inicial do Servidor", - "initialSetupDescription": "Crie a conta de administrador inicial do servidor. Apenas um administrador do servidor pode existir. Você sempre pode alterar essas credenciais posteriormente.", - "createAdminAccount": "Criar Conta de Administrador", - "setupErrorCreateAdmin": "Ocorreu um erro ao criar a conta de administrador do servidor.", - "certificateStatus": "Status do Certificado", - "loading": "Carregando", - "restart": "Reiniciar", - "domains": "Domínios", - "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", - "domainCreate": "Criar Domínio", - "domainCreatedDescription": "Domínio criado com sucesso", - "domainDeletedDescription": "Domínio deletado com sucesso", - "domainQuestionRemove": "Tem certeza de que deseja remover o domínio {domain} da sua conta?", - "domainMessageRemove": "Uma vez removido, o domínio não estará mais associado à sua conta.", - "domainMessageConfirm": "Para confirmar, digite o nome do domínio abaixo.", - "domainConfirmDelete": "Confirmar Exclusão de Domínio", - "domainDelete": "Excluir Domínio", - "domain": "Domínio", - "selectDomainTypeNsName": "Delegação de Domínio (NS)", - "selectDomainTypeNsDescription": "Este domínio e todos os seus subdomínios. Use isso quando quiser controlar uma zona de domínio inteira.", - "selectDomainTypeCnameName": "Domínio Único (CNAME)", - "selectDomainTypeCnameDescription": "Apenas este domínio específico. Use isso para subdomínios individuais ou entradas de domínio específicas.", - "selectDomainTypeWildcardName": "Domínio Coringa", - "selectDomainTypeWildcardDescription": "Este domínio e seus subdomínios.", - "domainDelegation": "Domínio Único", - "selectType": "Selecione um tipo", - "actions": "Ações", - "refresh": "Atualizar", - "refreshError": "Falha ao atualizar dados", - "verified": "Verificado", - "pending": "Pendente", - "sidebarBilling": "Faturamento", - "billing": "Faturamento", - "orgBillingDescription": "Gerir suas informações de faturação e assinaturas", - "github": "GitHub", - "pangolinHosted": "Hospedagem Pangolin", - "fossorial": "Fossorial", - "completeAccountSetup": "Completar Configuração da Conta", - "completeAccountSetupDescription": "Defina sua senha para começar", - "accountSetupSent": "Enviaremos um código de ativação da conta para este endereço de e-mail.", - "accountSetupCode": "Código de Ativação", - "accountSetupCodeDescription": "Verifique seu e-mail para obter o código de ativação.", - "passwordCreate": "Criar Senha", - "passwordCreateConfirm": "Confirmar Senha", - "accountSetupSubmit": "Enviar Código de Ativação", - "completeSetup": "Configuração Completa", - "accountSetupSuccess": "Configuração da conta concluída! Bem-vindo ao Pangolin!", - "documentation": "Documentação", - "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", - "settingsErrorUpdateDescription": "Ocorreu um erro ao atualizar configurações", - "sidebarCollapse": "Recolher", - "sidebarExpand": "Expandir", - "newtUpdateAvailable": "Nova Atualização Disponível", - "newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.", - "domainPickerEnterDomain": "Domínio", - "domainPickerPlaceholder": "myapp.exemplo.com", - "domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.", - "domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis", - "domainPickerTabAll": "Todos", - "domainPickerTabOrganization": "Organização", - "domainPickerTabProvided": "Fornecido", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Verificando disponibilidade...", - "domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado. Tente um domínio diferente ou verifique as configurações do domínio da sua organização.", - "domainPickerOrganizationDomains": "Domínios da Organização", - "domainPickerProvidedDomains": "Domínios Fornecidos", - "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": "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", - "securityKeyNone": "Nenhuma chave de segurança registrada", - "securityKeyNameRequired": "Nome é obrigatório", - "securityKeyRemove": "Remover", - "securityKeyLastUsed": "Último uso: {date}", - "securityKeyNameLabel": "Nome", - "securityKeyRegisterSuccess": "Chave de segurança registrada com sucesso", - "securityKeyRegisterError": "Erro ao registrar chave de segurança", - "securityKeyRemoveSuccess": "Chave de segurança removida com sucesso", - "securityKeyRemoveError": "Erro ao remover chave de segurança", - "securityKeyLoadError": "Erro ao carregar chaves de segurança", - "securityKeyLogin": "Continuar com a chave de segurança", - "securityKeyAuthError": "Erro ao autenticar com chave de segurança", - "securityKeyRecommendation": "Considere registrar outra chave de segurança em um dispositivo diferente para garantir que você não fique bloqueado da sua conta.", - "registering": "Registrando...", - "securityKeyPrompt": "Verifique sua identidade usando sua chave de segurança. Certifique-se de que sua chave de segurança está conectada e pronta.", - "securityKeyBrowserNotSupported": "Seu navegador não suporta chaves de segurança. Use um navegador moderno como Chrome, Firefox ou Safari.", - "securityKeyPermissionDenied": "Permita o acesso à sua chave de segurança para continuar o login.", - "securityKeyRemovedTooQuickly": "Mantenha sua chave de segurança conectada até que o processo de login seja concluído.", - "securityKeyNotSupported": "Sua chave de segurança pode não ser compatível. Tente uma chave de segurança diferente.", - "securityKeyUnknownError": "Houve um problema ao usar sua chave de segurança. Tente novamente.", - "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.", - "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", - "securityKeyTwoFactorRequired": "Autenticação de Dois Fatores Obrigatória", - "securityKeyTwoFactorDescription": "Insira seu código de autenticação de dois fatores para registrar a chave de segurança", - "securityKeyTwoFactorRemoveDescription": "Insira seu código de autenticação de dois fatores para remover a chave de segurança", - "securityKeyTwoFactorCode": "Código de Dois Fatores", - "securityKeyRemoveTitle": "Remover Chave de Segurança", - "securityKeyRemoveDescription": "Insira sua senha para remover a chave de segurança \"{name}\"", - "securityKeyNoKeysRegistered": "Nenhuma chave de segurança registrada", - "securityKeyNoKeysDescription": "Adicione uma chave de segurança para melhorar a segurança da sua conta", - "createDomainRequired": "Domínio é obrigatório", - "createDomainAddDnsRecords": "Adicionar Registros DNS", - "createDomainAddDnsRecordsDescription": "Adicione os seguintes registros DNS ao seu provedor de domínio para completar a configuração.", - "createDomainNsRecords": "Registros NS", - "createDomainRecord": "Registrar", - "createDomainType": "Tipo:", - "createDomainName": "Nome:", - "createDomainValue": "Valor:", - "createDomainCnameRecords": "Registros CNAME", - "createDomainARecords": "Registros A", - "createDomainRecordNumber": "Registrar {number}", - "createDomainTxtRecords": "Registros TXT", - "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", - "and": "e", - "privacyPolicy": "política de privacidade" - }, - "siteRequired": "Site é obrigatório.", - "olmTunnel": "Olm Tunnel", - "olmTunnelDescription": "Use Olm para conectividade do cliente", - "errorCreatingClient": "Erro ao criar cliente", - "clientDefaultsNotFound": "Padrões do cliente não encontrados", - "createClient": "Criar Cliente", - "createClientDescription": "Crie um novo cliente para conectar aos seus sites", - "seeAllClients": "Ver Todos os Clientes", - "clientInformation": "Informações do Cliente", - "clientNamePlaceholder": "Nome do cliente", - "address": "Endereço", - "subnetPlaceholder": "Sub-rede", - "addressDescription": "O endereço que este cliente usará para conectividade", - "selectSites": "Selecionar sites", - "sitesDescription": "O cliente terá conectividade com os sites selecionados", - "clientInstallOlm": "Instalar Olm", - "clientInstallOlmDescription": "Execute o Olm em seu sistema", - "clientOlmCredentials": "Credenciais Olm", - "clientOlmCredentialsDescription": "É assim que Olm se autenticará com o servidor", - "olmEndpoint": "Endpoint Olm", - "olmId": "ID Olm", - "olmSecretKey": "Chave Secreta Olm", - "clientCredentialsSave": "Salve suas Credenciais", - "clientCredentialsSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-las para um local seguro.", - "generalSettingsDescription": "Configure as configurações gerais para este cliente", - "clientUpdated": "Cliente atualizado", - "clientUpdatedDescription": "O cliente foi atualizado.", - "clientUpdateFailed": "Falha ao atualizar cliente", - "clientUpdateError": "Ocorreu um erro ao atualizar o cliente.", - "sitesFetchFailed": "Falha ao buscar sites", - "sitesFetchError": "Ocorreu um erro ao buscar sites.", - "olmErrorFetchReleases": "Ocorreu um erro ao buscar lançamentos do Olm.", - "olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.", - "remoteSubnets": "Sub-redes Remotas", - "enterCidrRange": "Insira o intervalo CIDR", - "remoteSubnetsDescription": "Adicionar intervalos CIDR que podem ser acessados deste site remotamente usando clientes. Use um formato como 10.0.0.0/24. Isso SOMENTE se aplica à conectividade do cliente VPN.", - "resourceEnableProxy": "Ativar Proxy Público", - "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", - "externalProxyEnabled": "Proxy Externo Habilitado", - "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", - "domainPickerBaseDomainLabel": "Domínio Base", - "domainPickerSearchDomains": "Buscar domínios...", - "domainPickerNoDomainsFound": "Nenhum domínio encontrado", - "domainPickerLoadingDomains": "Carregando domínios...", - "domainPickerSelectBaseDomain": "Selecione o domínio base...", - "domainPickerNotAvailableForCname": "Não disponível para domínios CNAME", - "domainPickerEnterSubdomainOrLeaveBlank": "Digite um subdomínio ou deixe em branco para usar o domínio base.", - "domainPickerEnterSubdomainToSearch": "Digite um subdomínio para buscar e selecionar entre os domínios gratuitos disponíveis.", - "domainPickerFreeDomains": "Domínios Gratuitos", - "domainPickerSearchForAvailableDomains": "Pesquise por domínios disponíveis", - "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", - "proxyPort": "Porta", - "resourcesTableProxyResources": "Recursos de Proxy", - "resourcesTableClientResources": "Recursos do Cliente", - "resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.", - "resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.", - "resourcesTableDestination": "Destino", - "resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com", - "resourcesTableClients": "Clientes", - "resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.", - "editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente", - "editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.", - "editInternalResourceDialogResourceProperties": "Propriedades do Recurso", - "editInternalResourceDialogName": "Nome", - "editInternalResourceDialogProtocol": "Protocolo", - "editInternalResourceDialogSitePort": "Porta do Site", - "editInternalResourceDialogTargetConfiguration": "Configuração do Alvo", - "editInternalResourceDialogCancel": "Cancelar", - "editInternalResourceDialogSaveResource": "Guardar Recurso", - "editInternalResourceDialogSuccess": "Sucesso", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno atualizado com sucesso", - "editInternalResourceDialogError": "Erro", - "editInternalResourceDialogFailedToUpdateInternalResource": "Falha ao atualizar recurso interno", - "editInternalResourceDialogNameRequired": "Nome é obrigatório", - "editInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres", - "editInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1", - "editInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido", - "editInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1", - "editInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536", - "createInternalResourceDialogNoSitesAvailable": "Nenhum Site Disponível", - "createInternalResourceDialogNoSitesAvailableDescription": "Você precisa ter pelo menos um site Newt com uma sub-rede configurada para criar recursos internos.", - "createInternalResourceDialogClose": "Fechar", - "createInternalResourceDialogCreateClientResource": "Criar Recurso do Cliente", - "createInternalResourceDialogCreateClientResourceDescription": "Crie um novo recurso que estará acessível aos clientes conectados ao site selecionado.", - "createInternalResourceDialogResourceProperties": "Propriedades do Recurso", - "createInternalResourceDialogName": "Nome", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Selecionar site...", - "createInternalResourceDialogSearchSites": "Procurar sites...", - "createInternalResourceDialogNoSitesFound": "Nenhum site encontrado.", - "createInternalResourceDialogProtocol": "Protocolo", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Porta do Site", - "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.", - "createInternalResourceDialogCancel": "Cancelar", - "createInternalResourceDialogCreateResource": "Criar Recurso", - "createInternalResourceDialogSuccess": "Sucesso", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno criado com sucesso", - "createInternalResourceDialogError": "Erro", - "createInternalResourceDialogFailedToCreateInternalResource": "Falha ao criar recurso interno", - "createInternalResourceDialogNameRequired": "Nome é obrigatório", - "createInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres", - "createInternalResourceDialogPleaseSelectSite": "Por favor, selecione um site", - "createInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1", - "createInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido", - "createInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1", - "createInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536", - "siteConfiguration": "Configuração", - "siteAcceptClientConnections": "Aceitar Conexões de Clientes", - "siteAcceptClientConnectionsDescription": "Permitir que outros dispositivos se conectem através desta instância Newt como um gateway usando clientes.", - "siteAddress": "Endereço do Site", - "siteAddressDescription": "Especificar o endereço IP do host para que os clientes se conectem. Este é o endereço interno do site na rede Pangolin para os clientes endereçarem. Deve estar dentro da sub-rede da Organização.", - "autoLoginExternalIdp": "Login Automático com IDP Externo", - "autoLoginExternalIdpDescription": "Redirecionar imediatamente o 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.", - "autoLoginTitle": "Redirecionando", - "autoLoginDescription": "Redirecionando você para o provedor de identidade externo para autenticação.", - "autoLoginProcessing": "Preparando autenticação...", - "autoLoginRedirecting": "Redirecionando para login...", - "autoLoginError": "Erro de Login Automático", - "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", - "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.", - "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", - "introTitle": "Pangolin Auto-Hospedado Gerenciado", - "introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.", - "introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin — seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:", - "benefitSimplerOperations": { - "title": "Operações simples", - "description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade." - }, - "benefitAutomaticUpdates": { - "title": "Atualizações automáticas", - "description": "O painel em nuvem evolui rapidamente, para que você obtenha novos recursos e correções de bugs sem ter de puxar manualmente novos contêineres toda vez." - }, - "benefitLessMaintenance": { - "title": "Menos manutenção", - "description": "Sem migrações, backups ou infraestrutura extra para gerir. Lidamos com isso na nuvem." - }, - "benefitCloudFailover": { - "title": "Falha na nuvem", - "description": "Se o seu nó descer, seus túneis podem falhar temporariamente nos nossos pontos de presença na nuvem até que você o traga online." - }, - "benefitHighAvailability": { - "title": "Alta disponibilidade (Ppos)", - "description": "Você também pode anexar vários nós à sua conta para um melhor desempenho." - }, - "benefitFutureEnhancements": { - "title": "Aprimoramentos futuros", - "description": "Estamos planejando adicionar mais análises, alertas e ferramentas de gerenciamento para tornar sua implantação ainda mais robusta." - }, - "docsAlert": { - "text": "Saiba mais sobre a opção Hospedagem Auto-Gerenciada no nosso", - "documentation": "documentação" - }, - "convertButton": "Converter este nó para Auto-Hospedado Gerenciado" - }, - "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", - "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", - "domainPickerUnverified": "Não verificado", - "domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.", - "domainPickerError": "ERRO", - "domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização", - "domainPickerErrorCheckAvailability": "Não foi possível verificar a disponibilidade do domínio", - "domainPickerInvalidSubdomain": "Subdomínio inválido", - "domainPickerInvalidSubdomainRemoved": "A entrada \"{sub}\" foi removida porque ela não é válida.", - "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", - "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 deleted file mode 100644 index 600725b6..00000000 --- a/messages/ru-RU.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Создайте свою организацию, сайт и ресурсы", - "setupNewOrg": "Новая организация", - "setupCreateOrg": "Создать организацию", - "setupCreateResources": "Создать ресурсы", - "setupOrgName": "Название организации", - "orgDisplayName": "Это отображаемое имя вашей организации.", - "orgId": "ID организации", - "setupIdentifierMessage": "Уникальный идентификатор вашей организации. Он задаётся отдельно от отображаемого имени.", - "setupErrorIdentifier": "ID организации уже занят. Выберите другой.", - "componentsErrorNoMemberCreate": "Вы пока не состоите ни в одной организации. Создайте организацию для начала работы.", - "componentsErrorNoMember": "Вы пока не состоите ни в одной организации.", - "welcome": "Добро пожаловать!", - "welcomeTo": "Добро пожаловать в", - "componentsCreateOrg": "Создать организацию", - "componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.", - "componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.", - "dismiss": "Отменить", - "componentsLicenseViolation": "Нарушение лицензии: Сервер использует {usedSites} сайтов, что превышает лицензионный лимит в {maxSites} сайтов. Соблюдайте условия лицензии для использования всех функций.", - "componentsSupporterMessage": "Спасибо за поддержку Pangolin в качестве {tier}!", - "inviteErrorNotValid": "Извините, но это приглашение не было принято или срок его действия истёк.", - "inviteErrorUser": "Извините, но приглашение, к которому вы пытаетесь получить доступ, предназначено не для этого пользователя.", - "inviteLoginUser": "Убедитесь, что вы вошли под правильным пользователем.", - "inviteErrorNoUser": "Извините, но похоже, что приглашение, к которому вы пытаетесь получить доступ, предназначено для несуществующего пользователя.", - "inviteCreateUser": "Сначала создайте аккаунт.", - "goHome": "На главную", - "inviteLogInOtherUser": "Войти под другим пользователем", - "createAnAccount": "Создать учётную запись", - "inviteNotAccepted": "Приглашение не принято", - "authCreateAccount": "Создайте учётную запись для начала работы", - "authNoAccount": "Нет учётной записи?", - "email": "Email", - "password": "Пароль", - "confirmPassword": "Подтвердите пароль", - "createAccount": "Создать учётную запись", - "viewSettings": "Посмотреть настройки", - "delete": "Удалить", - "name": "Имя", - "online": "Онлайн", - "offline": "Офлайн", - "site": "Сайт", - "dataIn": "Входящий трафик", - "dataOut": "Исходящий трафик", - "connectionType": "Тип соединения", - "tunnelType": "Тип туннеля", - "local": "Локальный", - "edit": "Редактировать", - "siteConfirmDelete": "Подтвердить удаление сайта", - "siteDelete": "Удалить сайт", - "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": "Локальные сайты не создают туннели, узнать больше", - "siteConfirmCopy": "Я скопировал(а) конфигурацию", - "searchSitesProgress": "Поиск сайтов...", - "siteAdd": "Добавить сайт", - "siteInstallNewt": "Установить Newt", - "siteInstallNewtDescription": "Запустите Newt в вашей системе", - "WgConfiguration": "Конфигурация WireGuard", - "WgConfigurationDescription": "Используйте следующую конфигурацию для подключения к вашей сети", - "operatingSystem": "Операционная система", - "commands": "Команды", - "recommended": "Рекомендуется", - "siteNewtDescription": "Для лучшего пользовательского опыта используйте Newt. Он использует WireGuard под капотом и позволяет обращаться к вашим приватным ресурсам по их LAN-адресу в вашей частной сети прямо из панели управления Pangolin.", - "siteRunsInDocker": "Работает в Docker", - "siteRunsInShell": "Работает в оболочке на macOS, Linux и Windows", - "siteErrorDelete": "Ошибка при удалении сайта", - "siteErrorUpdate": "Не удалось обновить сайт", - "siteErrorUpdateDescription": "Произошла ошибка при обновлении сайта.", - "siteUpdated": "Сайт обновлён", - "siteUpdatedDescription": "Сайт был успешно обновлён.", - "siteGeneralDescription": "Настройте общие параметры для этого сайта", - "siteSettingDescription": "Настройте параметры вашего сайта", - "siteSetting": "Настройки {siteName}", - "siteNewtTunnel": "Туннель Newt (Рекомендуется)", - "siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.", - "siteWg": "Базовый WireGuard", - "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", - "siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", - "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", - "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": "Ваш токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Он должен передаваться клиентом при каждом запросе для аутентификации.", - "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": "Отсутствует ID организации", - "orgMissingMessage": "Невозможно восстановить приглашение без ID организации.", - "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 {# день} few {# дня} many {# дней} 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": "Подтвердить удаление", - "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": "Community Edition", - "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": "ID хоста", - "licenseReckeckAll": "Перепроверить все ключи", - "licenseSiteUsage": "Использование сайтов", - "licenseSiteUsageDecsription": "Просмотр количества сайтов, использующих эту лицензию.", - "licenseNoSiteLimit": "Нет ограничения на количество сайтов при использовании нелицензированного хоста.", - "licensePurchase": "Приобрести лицензию", - "licensePurchaseSites": "Приобрести дополнительные сайты", - "licenseSitesUsedMax": "Использовано сайтов: {usedSites} из {maxSites}", - "licenseSitesUsed": "{count, plural, =0 {0 сайтов} one {# сайт} few {# сайта} many {# сайтов} other {# сайтов}} в системе.", - "licensePurchaseDescription": "Выберите, для скольких сайтов вы хотите {selectedMode, select, license {приобрести лицензию. Вы всегда можете добавить больше сайтов позже.} other {добавить к существующей лицензии.}}", - "licenseFee": "Лицензионный сбор", - "licensePriceSite": "Цена за сайт", - "total": "Итого", - "licenseContinuePayment": "Перейти к оплате", - "pricingPage": "страница цен", - "pricingPortal": "Посмотреть портал покупок", - "licensePricingPage": "Для актуальных цен и скидок посетите ", - "invite": "Приглашения", - "inviteRegenerate": "Пересоздать приглашение", - "inviteRegenerateDescription": "Отозвать предыдущее приглашение и создать новое", - "inviteRemove": "Удалить приглашение", - "inviteRemoveError": "Не удалось удалить приглашение", - "inviteRemoveErrorDescription": "Произошла ошибка при удалении приглашения.", - "inviteRemoved": "Приглашение удалено", - "inviteRemovedDescription": "Приглашение для {email} было удалено.", - "inviteQuestionRemove": "Вы уверены, что хотите удалить приглашение {email}?", - "inviteMessageRemove": "После удаления это приглашение больше не будет действительным. Вы всегда можете пригласить пользователя заново.", - "inviteMessageConfirm": "Для подтверждения введите email адрес приглашения ниже.", - "inviteQuestionRegenerate": "Вы уверены, что хотите пересоздать приглашение для {email}? Это отзовёт предыдущее приглашение.", - "inviteRemoveConfirm": "Подтвердить удаление приглашения", - "inviteRegenerated": "Приглашение пересоздано", - "inviteSent": "Новое приглашение отправлено {email}.", - "inviteSentEmail": "Отправить email уведомление пользователю", - "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": "Неверный адрес Email", - "inviteValidityDuration": "Пожалуйста, выберите продолжительность", - "accessRoleSelectPlease": "Пожалуйста, выберите роль", - "usernameRequired": "Имя пользователя обязательно", - "idpSelectPlease": "Пожалуйста, выберите Identity Provider", - "idpGenericOidc": "Обычный OAuth2/OIDC provider.", - "accessRoleErrorFetch": "Не удалось получить роли", - "accessRoleErrorFetchDescription": "Произошла ошибка при получении ролей", - "idpErrorFetch": "Не удалось получить идентификатор провайдера", - "idpErrorFetchDescription": "Произошла ошибка при получении поставщиков удостоверений", - "userErrorExists": "Пользователь уже существует", - "userErrorExistsDescription": "Этот пользователь уже является участником организации.", - "inviteError": "Не удалось пригласить пользователя", - "inviteErrorDescription": "Произошла ошибка при приглашении пользователя", - "userInvited": "Пользователь приглашён", - "userInvitedDescription": "Пользователь был успешно приглашён.", - "userErrorCreate": "Не удалось создать пользователя", - "userErrorCreateDescription": "Произошла ошибка при создании пользователя", - "userCreated": "Пользователь создан", - "userCreatedDescription": "Пользователь был успешно создан.", - "userTypeInternal": "Внутренний пользователь", - "userTypeInternalDescription": "Пригласите пользователя напрямую в вашу организацию.", - "userTypeExternal": "Внешний пользователь", - "userTypeExternalDescription": "Создайте пользователя через внешний Identity Provider.", - "accessUserCreateDescription": "Следуйте инструкциям ниже для создания нового пользователя", - "userSeeAll": "Просмотр всех пользователей", - "userTypeTitle": "Тип пользователя", - "userTypeDescription": "Выберите способ создание пользователя", - "userSettings": "Информация о пользователе", - "userSettingsDescription": "Введите сведения о новом пользователе", - "inviteEmailSent": "Отправить приглашение по Email", - "inviteValid": "Действительно", - "selectDuration": "Укажите срок действия", - "accessRoleSelect": "Выберите роль", - "inviteEmailSentDescription": "Email был отправлен пользователю со ссылкой доступа ниже. Он должен перейти по ссылке для принятия приглашения.", - "inviteSentDescription": "Пользователь был приглашён. Он должен перейти по ссылке ниже для принятия приглашения.", - "inviteExpiresIn": "Приглашение истечёт через {days, plural, one {# день} few {# дня} many {# дней} other {# дней}}.", - "idpTitle": "Поставщик удостоверений", - "idpSelect": "Выберите поставщика удостоверений для внешнего пользователя", - "idpNotConfigured": "Поставщики удостоверений не настроены. Пожалуйста, настройте поставщика удостоверений перед созданием внешних пользователей.", - "usernameUniq": "Это должно соответствовать уникальному имени пользователя, существующему в выбранном поставщике удостоверений.", - "emailOptional": "Email (необязательно)", - "nameOptional": "Имя (необязательно)", - "accessControls": "Контроль доступа", - "userDescription2": "Управление настройками этого пользователя", - "accessRoleErrorAdd": "Не удалось добавить пользователя в роль", - "accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.", - "userSaved": "Пользователь сохранён", - "userSavedDescription": "Пользователь был обновлён.", - "autoProvisioned": "Автоподбор", - "autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем", - "accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации", - "accessControlsSubmit": "Сохранить контроль доступа", - "roles": "Роли", - "accessUsersRoles": "Управление пользователями и ролями", - "accessUsersRolesDescription": "Приглашайте пользователей и добавляйте их в роли для управления доступом к вашей организации", - "key": "Ключ", - "createdAt": "Создано в", - "proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.", - "proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.", - "proxyEnableSSL": "Включить SSL", - "proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.", - "target": "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": "Пользовательский заголовок Host", - "proxyCustomHeaderDescription": "Заголовок host для установки при проксировании запросов. Оставьте пустым для использования по умолчанию.", - "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", - "architecture": "Архитектура", - "sites": "Сайты", - "siteWgAnyClients": "Используйте любой клиент WireGuard для подключения. Вам придётся обращаться к вашим внутренним ресурсам, используя IP узла.", - "siteWgCompatibleAllClients": "Совместим со всеми клиентами WireGuard", - "siteWgManualConfigurationRequired": "Требуется ручная настройка", - "userErrorNotAdminOrOwner": "Пользователь не является администратором или владельцем", - "pangolinSettings": "Настройки - Pangolin", - "accessRoleYour": "Ваша роль:", - "accessRoleSelect2": "Выберите роль", - "accessUserSelect": "Выберите пользователя", - "otpEmailEnter": "Введите email", - "otpEmailEnterDescription": "Нажмите enter для добавления email после ввода в поле.", - "otpEmailErrorInvalid": "Неверный email адрес. Подстановочный знак (*) должен быть всей локальной частью.", - "otpEmailSmtpRequired": "Требуется SMTP", - "otpEmailSmtpRequiredDescription": "SMTP должен быть включён на сервере для использования аутентификации с одноразовым паролем.", - "otpEmailTitle": "Одноразовые пароли", - "otpEmailTitleDescription": "Требовать аутентификацию на основе email для доступа к ресурсу", - "otpEmailWhitelist": "Белый список email", - "otpEmailWhitelistList": "Email адреса в белом списке", - "otpEmailWhitelistListDescription": "Только пользователи с этими email адресами смогут получить доступ к этому ресурсу. Им будет предложено ввести одноразовый пароль, отправленный на их email. Можно использовать подстановочные знаки (*@example.com) для разрешения любого email адреса с домена.", - "otpEmailWhitelistSave": "Сохранить белый список", - "passwordAdd": "Добавить пароль", - "passwordRemove": "Удалить пароль", - "pincodeAdd": "Добавить PIN-код", - "pincodeRemove": "Удалить PIN-код", - "resourceAuthMethods": "Методы аутентификации", - "resourceAuthMethodsDescriptions": "Разрешить доступ к ресурсу через дополнительные методы аутентификации", - "resourceAuthSettingsSave": "Успешно сохранено", - "resourceAuthSettingsSaveDescription": "Настройки аутентификации сохранены", - "resourceErrorAuthFetch": "Не удалось получить данные", - "resourceErrorAuthFetchDescription": "Произошла ошибка при получении данных", - "resourceErrorPasswordRemove": "Ошибка при удалении пароля ресурса", - "resourceErrorPasswordRemoveDescription": "Произошла ошибка при удалении пароля ресурса", - "resourceErrorPasswordSetup": "Ошибка при установке пароля ресурса", - "resourceErrorPasswordSetupDescription": "Произошла ошибка при установке пароля ресурса", - "resourceErrorPincodeRemove": "Ошибка при удалении PIN-кода ресурса", - "resourceErrorPincodeRemoveDescription": "Произошла ошибка при удалении PIN-кода ресурса", - "resourceErrorPincodeSetup": "Ошибка при установке PIN-кода ресурса", - "resourceErrorPincodeSetupDescription": "Произошла ошибка при установке PIN-кода ресурса", - "resourceErrorUsersRolesSave": "Не удалось установить роли", - "resourceErrorUsersRolesSaveDescription": "Произошла ошибка при установке ролей", - "resourceErrorWhitelistSave": "Не удалось сохранить белый список", - "resourceErrorWhitelistSaveDescription": "Произошла ошибка при сохранении белого списка", - "resourcePasswordSubmit": "Включить защиту паролем", - "resourcePasswordProtection": "Защита паролем {status}", - "resourcePasswordRemove": "Пароль ресурса удалён", - "resourcePasswordRemoveDescription": "Пароль ресурса был успешно удалён", - "resourcePasswordSetup": "Пароль ресурса установлен", - "resourcePasswordSetupDescription": "Пароль ресурса был успешно установлен", - "resourcePasswordSetupTitle": "Установить пароль", - "resourcePasswordSetupTitleDescription": "Установите пароль для защиты этого ресурса", - "resourcePincode": "PIN-код", - "resourcePincodeSubmit": "Включить защиту PIN-кодом", - "resourcePincodeProtection": "Защита PIN-кодом {status}", - "resourcePincodeRemove": "PIN-код ресурса удалён", - "resourcePincodeRemoveDescription": "PIN-код ресурса был успешно удалён", - "resourcePincodeSetup": "PIN-код ресурса установлен", - "resourcePincodeSetupDescription": "PIN-код ресурса был успешно установлен", - "resourcePincodeSetupTitle": "Установить PIN-код", - "resourcePincodeSetupTitleDescription": "Установите PIN-код для защиты этого ресурса", - "resourceRoleDescription": "Администраторы всегда имеют доступ к этому ресурсу.", - "resourceUsersRoles": "Пользователи и роли", - "resourceUsersRolesDescription": "Выберите пользователей и роли с доступом к этому ресурсу", - "resourceUsersRolesSubmit": "Сохранить пользователей и роли", - "resourceWhitelistSave": "Успешно сохранено", - "resourceWhitelistSaveDescription": "Настройки белого списка были сохранены", - "ssoUse": "Использовать Platform 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": "Администратор сервера - Pangolin", - "licenseTierProfessional": "Профессиональная лицензия", - "licenseTierEnterprise": "Корпоративная лицензия", - "licenseTierPersonal": "Personal License", - "licensed": "Лицензировано", - "yes": "Да", - "no": "Нет", - "sitesAdditional": "Дополнительные сайты", - "licenseKeys": "Лицензионные ключи", - "sitestCountDecrease": "Уменьшить количество сайтов", - "sitestCountIncrease": "Увеличить количество сайтов", - "idpManage": "Управление поставщиками удостоверений", - "idpManageDescription": "Просмотр и управление поставщиками удостоверений в системе", - "idpDeletedDescription": "Поставщик удостоверений успешно удалён", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Вы уверены, что хотите навсегда удалить поставщика удостоверений {name}?", - "idpMessageRemove": "Это удалит поставщика удостоверений и все связанные конфигурации. Пользователи, которые аутентифицируются через этого поставщика, больше не смогут войти.", - "idpMessageConfirm": "Для подтверждения введите имя поставщика удостоверений ниже.", - "idpConfirmDelete": "Подтвердить удаление поставщика удостоверений", - "idpDelete": "Удалить поставщика удостоверений", - "idp": "Поставщики удостоверений", - "idpSearch": "Поиск поставщиков удостоверений...", - "idpAdd": "Добавить поставщика удостоверений", - "idpClientIdRequired": "ID клиента обязателен.", - "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 ID клиента от вашего поставщика удостоверений", - "idpClientSecret": "Секрет клиента", - "idpClientSecretDescription": "OAuth2 секрет клиента от вашего поставщика удостоверений", - "idpAuthUrl": "URL авторизации", - "idpAuthUrlDescription": "URL конечной точки авторизации OAuth2", - "idpTokenUrl": "URL токена", - "idpTokenUrlDescription": "URL конечной точки токена OAuth2", - "idpOidcConfigureAlert": "Важная информация", - "idpOidcConfigureAlertDescription": "После создания поставщика удостоверений вам нужно будет настроить URL обратного вызова в настройках вашего поставщика удостоверений. URL обратного вызова будет предоставлен после успешного создания.", - "idpToken": "Конфигурация токена", - "idpTokenDescription": "Настройте, как извлекать информацию о пользователе из ID токена", - "idpJmespathAbout": "О JMESPath", - "idpJmespathAboutDescription": "Пути ниже используют синтаксис JMESPath для извлечения значений из ID токена.", - "idpJmespathAboutDescriptionLink": "Узнать больше о JMESPath", - "idpJmespathLabel": "Путь идентификатора", - "idpJmespathLabelDescription": "Путь к идентификатору пользователя в ID токене", - "idpJmespathEmailPathOptional": "Путь к email (необязательно)", - "idpJmespathEmailPathOptionalDescription": "Путь к email пользователя в 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": "Политики организации используются для контроля доступа к организациям на основе ID токена пользователя. Вы можете указать выражения JMESPath для извлечения информации о роли и организации из ID токена.", - "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": "Аутентификация - Pangolin", - "verificationCodeLengthRequirements": "Ваш код подтверждения должен состоять из 8 символов.", - "errorOccurred": "Произошла ошибка", - "emailErrorVerify": "Не удалось подтвердить email:", - "emailVerified": "Email успешно подтверждён! Перенаправляем вас...", - "verificationCodeErrorResend": "Не удалось повторно отправить код подтверждения:", - "verificationCodeResend": "Код подтверждения отправлен повторно", - "verificationCodeResendDescription": "Мы повторно отправили код подтверждения на ваш email адрес. Пожалуйста, проверьте вашу почту.", - "emailVerify": "Подтвердить email", - "emailVerifyDescription": "Введите код подтверждения, отправленный на ваш email адрес.", - "verificationCode": "Код подтверждения", - "verificationCodeEmailSent": "Мы отправили код подтверждения на ваш email адрес.", - "submit": "Отправить", - "emailVerifyResendProgress": "Отправка повторно...", - "emailVerifyResend": "Не получили код? Нажмите здесь для повторной отправки", - "passwordNotMatch": "Пароли не совпадают", - "signupError": "Произошла ошибка при регистрации", - "pangolinLogoAlt": "Логотип Pangolin", - "inviteAlready": "Похоже, вы были приглашены!", - "inviteAlreadyDescription": "Чтобы принять приглашение, вы должны войти или создать учётную запись.", - "signupQuestion": "Уже есть учётная запись?", - "login": "Войти", - "resourceNotFound": "Ресурс не найден", - "resourceNotFoundDescription": "Ресурс, к которому вы пытаетесь получить доступ, не существует.", - "pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр", - "pincodeRequirementsChars": "PIN должен содержать только цифры", - "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 был отправлен на ваш email", - "otpEmailErrorAuthenticate": "Не удалось аутентифицироваться с email", - "pincodeErrorAuthenticate": "Не удалось аутентифицироваться с PIN-кодом", - "passwordErrorAuthenticate": "Не удалось аутентифицироваться с паролем", - "poweredBy": "Разработано", - "authenticationRequired": "Требуется аутентификация", - "authenticationMethodChoose": "Выберите предпочтительный метод для доступа к {name}", - "authenticationRequest": "Вы должны аутентифицироваться для доступа к {name}", - "user": "Пользователь", - "pincodeInput": "6-значный PIN-код", - "pincodeSubmit": "Войти с PIN-кодом", - "passwordSubmit": "Войти с паролем", - "otpEmailDescription": "Одноразовый код будет отправлен на этот email.", - "otpEmailSend": "Отправить одноразовый код", - "otpEmail": "Одноразовый пароль (OTP)", - "otpEmailSubmit": "Отправить OTP", - "backToEmail": "Назад к email", - "noSupportKey": "Сервер работает без ключа поддержки. Подумайте о поддержке проекта!", - "accessDenied": "Доступ запрещён", - "accessDeniedDescription": "Вам не разрешён доступ к этому ресурсу. Если это ошибка, пожалуйста, свяжитесь с администратором.", - "accessTokenError": "Ошибка проверки токена доступа", - "accessGranted": "Доступ предоставлен", - "accessUrlInvalid": "Неверный URL доступа", - "accessGrantedDescription": "Вам был предоставлен доступ к этому ресурсу. Перенаправляем вас...", - "accessUrlInvalidDescription": "Этот общий URL доступа недействителен. Пожалуйста, свяжитесь с владельцем ресурса для получения нового URL.", - "tokenInvalid": "Неверный токен", - "pincodeInvalid": "Неверный код", - "passwordErrorRequestReset": "Не удалось запросить сброс:", - "passwordErrorReset": "Не удалось сбросить пароль:", - "passwordResetSuccess": "Пароль успешно сброшен! Вернуться к входу...", - "passwordReset": "Сброс пароля", - "passwordResetDescription": "Следуйте инструкциям для сброса вашего пароля", - "passwordResetSent": "Мы отправим код сброса пароля на этот email адрес.", - "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": "IdP не найден", - "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} ТБ", - "gigabytes": "{count} ГБ", - "megabytes": "{count} МБ", - "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": "Обновить пользователя Org", - "createOrgUser": "Создать пользователя Org", - "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": "Настроить белый список ресурсов email", - "actionGetResourceEmailWhitelist": "Получить белый список ресурсов email", - "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": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.", - "enableDockerSocketLink": "Узнать больше", - "viewDockerContainers": "Просмотр контейнеров Docker", - "containersIn": "Контейнеры в {siteName}", - "selectContainerDescription": "Выберите любой контейнер для использования в качестве имени хоста для этой цели. Нажмите на порт, чтобы использовать порт.", - "containerName": "Имя", - "containerImage": "Образ", - "containerState": "Состояние", - "containerNetworks": "Сети", - "containerHostnameIp": "Имя хоста/IP", - "containerLabels": "Метки", - "containerLabelsCount": "{count, plural, one {# метка} few {# метки} many {# меток} other {# меток}}", - "containerLabelsTitle": "Метки контейнера", - "containerLabelEmpty": "", - "containerPorts": "Порты", - "containerPortsMore": "+{count} ещё", - "containerActions": "Действия", - "select": "Выбрать", - "noContainersMatchingFilters": "Контейнеры, соответствующие текущим фильтрам, не найдены.", - "showContainersWithoutPorts": "Показать контейнеры без портов", - "showStoppedContainers": "Показать остановленные контейнеры", - "noContainersFound": "Контейнеры не найдены. Убедитесь, что контейнеры Docker запущены.", - "searchContainersPlaceholder": "Поиск среди {count} {count, plural, one {контейнера} few {контейнеров} many {контейнеров} other {контейнеров}}...", - "searchResultsCount": "{count, plural, one {# результат} few {# результата} many {# результатов} 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", - "fossorial": "Fossorial", - "completeAccountSetup": "Завершите настройку аккаунта", - "completeAccountSetupDescription": "Установите ваш пароль, чтобы начать", - "accountSetupSent": "Мы отправим код для настройки аккаунта на этот email адрес.", - "accountSetupCode": "Код настройки", - "accountSetupCodeDescription": "Проверьте вашу почту для получения кода настройки.", - "passwordCreate": "Создать пароль", - "passwordCreateConfirm": "Подтвердите пароль", - "accountSetupSubmit": "Отправить код настройки", - "completeSetup": "Завершить настройку", - "accountSetupSuccess": "Настройка аккаунта завершена! Добро пожаловать в Pangolin!", - "documentation": "Документация", - "saveAllSettings": "Сохранить все настройки", - "settingsUpdated": "Настройки обновлены", - "settingsUpdatedDescription": "Все настройки успешно обновлены", - "settingsErrorUpdate": "Не удалось обновить настройки", - "settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек", - "sidebarCollapse": "Свернуть", - "sidebarExpand": "Развернуть", - "newtUpdateAvailable": "Доступно обновление", - "newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.", - "domainPickerEnterDomain": "Домен", - "domainPickerPlaceholder": "myapp.example.com", - "domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.", - "domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции", - "domainPickerTabAll": "Все", - "domainPickerTabOrganization": "Организация", - "domainPickerTabProvided": "Предоставлено", - "domainPickerSortAsc": "А-Я", - "domainPickerSortDesc": "Я-А", - "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 минут равны одному сайту, работающему круглосуточно за весь месяц. Когда вы достигните лимита, ваши сайты будут отключаться до тех пор, пока вы не обновите тарифный план или не сократите нагрузку. При использовании узлов не тарифицируется.", - "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": "Я согласен с", - "termsOfService": "условия использования", - "and": "и", - "privacyPolicy": "политика конфиденциальности" - }, - "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", - "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": "Заголовки новой строки, разделённые: название заголовка: значение", - "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": "Порт сайта", - "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-адрес хоста для подключения клиентов. Это внутренний адрес сайта в сети Pangolin для адресации клиентов. Должен находиться в пределах подсети организационного уровня.", - "autoLoginExternalIdp": "Автоматический вход с внешним провайдером", - "autoLoginExternalIdpDescription": "Немедленно перенаправьте пользователя к внешнему провайдеру для аутентификации.", - "selectIdp": "Выберите провайдера", - "selectIdpPlaceholder": "Выберите провайдера...", - "selectIdpRequired": "Пожалуйста, выберите провайдера, когда автоматический вход включен.", - "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": "Подтвердите удаление узла", - "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 с дополнительными колокольнями и свистками", - "introTitle": "Управляемый Само-Хост Панголина", - "introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.", - "introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin — туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:", - "benefitSimplerOperations": { - "title": "Более простые операции", - "description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки." - }, - "benefitAutomaticUpdates": { - "title": "Автоматическое обновление", - "description": "Панель управления в облаке развивается быстро, так что вы получаете новые функции и исправления ошибок, без необходимости каждый раз получать новые контейнеры." - }, - "benefitLessMaintenance": { - "title": "Меньше обслуживания", - "description": "Нет миграции баз данных, резервных копий или дополнительной инфраструктуры для управления. Мы обрабатываем это в облаке." - }, - "benefitCloudFailover": { - "title": "Облачное срабатывание", - "description": "Если ваш узел исчезнет, ваши туннели могут временно прерваться до наших облачных точек присутствия, пока вы не вернете его в сети." - }, - "benefitHighAvailability": { - "title": "Высокая доступность (PoP)", - "description": "Вы также можете прикрепить несколько узлов к вашему аккаунту для избыточности и лучшей производительности." - }, - "benefitFutureEnhancements": { - "title": "Будущие улучшения", - "description": "Мы планируем добавить дополнительные инструменты аналитики, оповещения и управления, чтобы сделать установку еще более надежной." - }, - "docsAlert": { - "text": "Узнайте больше о опции Managed Self-Hosted в нашей", - "documentation": "документация" - }, - "convertButton": "Конвертировать этот узел в управляемый себе-хост" - }, - "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", - "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": "Требуется подтверждение адреса электронной почты. Пожалуйста, войдите снова через {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 deleted file mode 100644 index b5b99888..00000000 --- a/messages/tr-TR.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "Organizasyonunuzu, sitenizi ve kaynaklarınızı oluşturun", - "setupNewOrg": "Yeni Organizasyon", - "setupCreateOrg": "Organizasyon Oluştur", - "setupCreateResources": "Kaynaklar Oluştur", - "setupOrgName": "Organizasyon Adı", - "orgDisplayName": "Bu, organizasyonunuzun görünen adıdır.", - "orgId": "Organizasyon ID", - "setupIdentifierMessage": "Bu, organizasyonunuzun benzersiz kimliğidir. Görünen adtan ayrı olarak.", - "setupErrorIdentifier": "Organizasyon ID'si zaten alınmış. Lütfen başka bir tane seçin.", - "componentsErrorNoMemberCreate": "Şu anda herhangi bir organizasyona üye değilsiniz. Başlamak için bir organizasyon oluşturun.", - "componentsErrorNoMember": "Şu anda herhangi bir organizasyona üye değilsiniz.", - "welcome": "Pangolin'e hoş geldiniz", - "welcomeTo": "Hoş geldiniz", - "componentsCreateOrg": "Bir Organizasyon Oluşturun", - "componentsMember": "{count, plural, =0 {hiçbir organizasyon} one {bir organizasyon} other {# organizasyon}} üyesisiniz.", - "componentsInvalidKey": "Geçersiz veya süresi dolmuş lisans anahtarları tespit edildi. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", - "dismiss": "Kapat", - "componentsLicenseViolation": "Lisans İhlali: Bu sunucu, lisanslı sınırı olan {maxSites} sitesini aşarak {usedSites} site kullanmaktadır. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", - "componentsSupporterMessage": "Pangolin'e {tier} olarak destek olduğunuz için teşekkür ederiz!", - "inviteErrorNotValid": "Üzgünüz, ancak erişmeye çalıştığınız davet kabul edilmemiş veya artık geçerli değil gibi görünüyor.", - "inviteErrorUser": "Üzgünüz, ancak erişmeye çalıştığınız davetin bu kullanıcı için olmadığı görünüyor.", - "inviteLoginUser": "Lütfen doğru kullanıcı olarak oturum açtığınızdan emin olun.", - "inviteErrorNoUser": "Üzgünüz, ancak erişmeye çalıştığınız davet, var olan bir kullanıcı için değil gibi görünüyor.", - "inviteCreateUser": "Öncelikle bir hesap oluşturun.", - "goHome": "Ana Sayfaya Dön", - "inviteLogInOtherUser": "Başka bir kullanıcı olarak giriş yapın", - "createAnAccount": "Bir Hesap Oluşturun", - "inviteNotAccepted": "Davet Kabul Edilmedi", - "authCreateAccount": "Başlamak için bir hesap oluşturun", - "authNoAccount": "Hesabınız yok mu?", - "email": "E-posta", - "password": "Şifre", - "confirmPassword": "Şifreyi Onayla", - "createAccount": "Hesap Oluştur", - "viewSettings": "Ayarları görüntüle", - "delete": "Sil", - "name": "Ad", - "online": "Çevrimiçi", - "offline": "Çevrimdışı", - "site": "Site", - "dataIn": "Gelen Veri", - "dataOut": "Giden Veri", - "connectionType": "Bağlantı Türü", - "tunnelType": "Tünel Türü", - "local": "Yerel", - "edit": "Düzenle", - "siteConfirmDelete": "Site Silmeyi Onayla", - "siteDelete": "Siteyi Sil", - "siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacak. Siteyle ilişkili tüm kaynaklar ve hedefler de kaldırılacaktır.", - "siteMessageConfirm": "Onaylamak için lütfen aşağıya sitenin adını yazın.", - "siteQuestionRemove": "{selectedSite} sitesini organizasyondan kaldırmak istediğinizden emin misiniz?", - "siteManageSites": "Siteleri Yönet", - "siteDescription": "Ağınıza güvenli tüneller üzerinden bağlantı izni verin", - "siteCreate": "Site Oluştur", - "siteCreateDescription2": "Yeni bir site oluşturup bağlanmak için aşağıdaki adımları izleyin", - "siteCreateDescription": "Kaynaklarınızı bağlamaya başlamak için yeni bir site oluşturun", - "close": "Kapat", - "siteErrorCreate": "Site oluşturulurken hata", - "siteErrorCreateKeyPair": "Anahtar çifti veya site varsayılanları bulunamadı", - "siteErrorCreateDefaults": "Site varsayılanları bulunamadı", - "method": "Yöntem", - "siteMethodDescription": "Bağlantıları nasıl açığa çıkaracağınız budur.", - "siteLearnNewt": "Newt'i sisteminize nasıl kuracağınızı öğrenin", - "siteSeeConfigOnce": "Konfigürasyonu yalnızca bir kez görebileceksiniz.", - "siteLoadWGConfig": "WireGuard yapılandırması yükleniyor...", - "siteDocker": "Docker Dağıtım Ayrıntılarını Genişlet", - "toggle": "Geçiş", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Çalıştır", - "siteLearnLocal": "Yerel siteler tünellemez, daha fazla bilgi edinin", - "siteConfirmCopy": "Yapılandırmayı kopyaladım", - "searchSitesProgress": "Siteleri ara...", - "siteAdd": "Site Ekle", - "siteInstallNewt": "Newt Yükle", - "siteInstallNewtDescription": "Newt'i sisteminizde çalıştırma", - "WgConfiguration": "WireGuard Yapılandırması", - "WgConfigurationDescription": "Ağınıza bağlanmak için aşağıdaki yapılandırmayı kullanın", - "operatingSystem": "İşletim Sistemi", - "commands": "Komutlar", - "recommended": "Önerilen", - "siteNewtDescription": "En iyi kullanıcı deneyimi için Newt'i kullanın. WireGuard'ı arka planda kullanır ve özel kaynaklarınıza Pangolin kontrol panelinden LAN adresleriyle erişmenizi sağlar.", - "siteRunsInDocker": "Docker'da Çalışır", - "siteRunsInShell": "macOS, Linux, ve Windows'da kabukta çalışır", - "siteErrorDelete": "Site silinirken hata", - "siteErrorUpdate": "Site güncellenirken hata oluştu", - "siteErrorUpdateDescription": "Site güncellenirken bir hata oluştu.", - "siteUpdated": "Site güncellendi", - "siteUpdatedDescription": "Site güncellendi.", - "siteGeneralDescription": "Bu site için genel ayarları yapılandırın", - "siteSettingDescription": "Sitenizdeki ayarları yapılandırın", - "siteSetting": "{siteName} Ayarları", - "siteNewtTunnel": "Newt Tüneli (Önerilen)", - "siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.", - "siteWg": "Temel WireGuard", - "siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.", - "siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", - "siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.", - "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", - "siteSeeAll": "Tüm Siteleri Gör", - "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", - "siteNewtCredentials": "Newt Kimlik Bilgileri", - "siteNewtCredentialsDescription": "Bu, Newt'in sunucu ile kimlik doğrulaması yapacağı yöntemdir", - "siteCredentialsSave": "Kimlik Bilgilerinizi Kaydedin", - "siteCredentialsSaveDescription": "Yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", - "siteInfo": "Site Bilgilendirmesi", - "status": "Durum", - "shareTitle": "Paylaşım Bağlantılarını Yönet", - "shareDescription": "Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun", - "shareSearch": "Paylaşım bağlantılarını ara...", - "shareCreate": "Paylaşım Bağlantısı Oluştur", - "shareErrorDelete": "Bağlantı silinirken hata oluştu", - "shareErrorDeleteMessage": "Bağlantı silinirken bir hata oluştu", - "shareDeleted": "Bağlantı silindi", - "shareDeletedDescription": "Bağlantı silindi", - "shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.", - "accessToken": "Erişim Jetonu", - "usageExamples": "Kullanım Örnekleri", - "tokenId": "Jeton ID", - "requestHeades": "İstek Başlıkları", - "queryParameter": "Sorgu Parametresi", - "importantNote": "Önemli Not", - "shareImportantDescription": "Güvenlik nedenleriyle, mümkünse başlıklar üzerinden kullanılması sorgu parametrelerinden daha önerilir, çünkü sorgu parametreleri sunucu günlüklerinde veya tarayıcı geçmişinde kaydedilebilir.", - "token": "Jeton", - "shareTokenSecurety": "Erişim jetonunuzu güvende tutun. Herkese açık alanlarda veya istemci tarafı kodunda paylaşmayın.", - "shareErrorFetchResource": "Kaynaklar getirilemedi", - "shareErrorFetchResourceDescription": "Kaynaklar getirilirken bir hata oluştu", - "shareErrorCreate": "Paylaşım bağlantısı oluşturma başarısız oldu", - "shareErrorCreateDescription": "Paylaşım bağlantısı oluşturulurken bir hata oluştu", - "shareCreateDescription": "Bu bağlantıya sahip olan herkes kaynağa erişebilir", - "shareTitleOptional": "Başlık (isteğe bağlı)", - "expireIn": "Süresi Dolacak", - "neverExpire": "Hiçbir Zaman Sona Ermez", - "shareExpireDescription": "Son kullanma süresi, bağlantının kullanılabilir ve kaynağa erişim sağlayacak süresidir. Bu süreden sonra bağlantı çalışmayı durduracak ve bu bağlantıyı kullanan kullanıcılar kaynağa erişimini kaybedecektir.", - "shareSeeOnce": "Bu bağlantıyı yalnızca bir kez görebileceksiniz. Kopyaladığınızdan emin olun.", - "shareAccessHint": "Bu bağlantıya sahip olan herkes kaynağa erişebilir. Dikkatle paylaşın.", - "shareTokenUsage": "Erişim Jetonu Kullanımını Gör", - "createLink": "Bağlantı Oluştur", - "resourcesNotFound": "Hiçbir kaynak bulunamadı", - "resourceSearch": "Kaynak ara", - "openMenu": "Menüyü Aç", - "resource": "Kaynak", - "title": "Başlık", - "created": "Oluşturulmuş", - "expires": "Süresi Doluyor", - "never": "Asla", - "shareErrorSelectResource": "Lütfen bir kaynak seçin", - "resourceTitle": "Kaynakları Yönet", - "resourceDescription": "Özel uygulamalarınıza güvenli vekil sunucular oluşturun", - "resourcesSearch": "Kaynakları ara...", - "resourceAdd": "Kaynak Ekle", - "resourceErrorDelte": "Kaynak silinirken hata", - "authentication": "Kimlik Doğrulama", - "protected": "Korunan", - "notProtected": "Korunmayan", - "resourceMessageRemove": "Kaldırıldıktan sonra kaynak artık erişilebilir olmayacaktır. Kaynakla ilişkili tüm hedefler de kaldırılacaktır.", - "resourceMessageConfirm": "Onaylamak için lütfen aşağıya kaynağın adını yazın.", - "resourceQuestionRemove": "{selectedResource} kaynağını organizasyondan kaldırmak istediğinizden emin misiniz?", - "resourceHTTP": "HTTPS Kaynağı", - "resourceHTTPDescription": "Bir alt alan adı veya temel alan adı kullanarak uygulamanıza HTTPS üzerinden vekil istek gönderin.", - "resourceRaw": "Ham TCP/UDP Kaynağı", - "resourceRawDescription": "Uygulamanıza TCP/UDP üzerinden port numarası ile vekil istek gönderin.", - "resourceCreate": "Kaynak Oluştur", - "resourceCreateDescription": "Yeni bir kaynak oluşturmak için aşağıdaki adımları izleyin", - "resourceSeeAll": "Tüm Kaynakları Gör", - "resourceInfo": "Kaynak Bilgilendirmesi", - "resourceNameDescription": "Bu, kaynak için görünen addır.", - "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", - "resourceHTTPSSettings": "HTTPS Ayarları", - "resourceHTTPSSettingsDescription": "Kaynağınıza HTTPS üzerinden erişimin nasıl sağlanacağını yapılandırın", - "domainType": "Alan Türü", - "subdomain": "Alt Alan Adı", - "baseDomain": "Temel Alan Adı", - "subdomnainDescription": "Kaynağınızın erişilebileceği alt alan adı.", - "resourceRawSettings": "TCP/UDP Ayarları", - "resourceRawSettingsDescription": "Kaynağınıza TCP/UDP üzerinden erişimin nasıl sağlanacağını yapılandırın", - "protocol": "Protokol", - "protocolSelect": "Bir protokol seçin", - "resourcePortNumber": "Port Numarası", - "resourcePortNumberDescription": "Vekil istekler için harici port numarası.", - "cancel": "İptal", - "resourceConfig": "Yapılandırma Parçaları", - "resourceConfigDescription": "TCP/UDP kaynağınızı kurmak için bu yapılandırma parçalarını kopyalayıp yapıştırın", - "resourceAddEntrypoints": "Traefik: Başlangıç Noktaları Ekleyin", - "resourceExposePorts": "Gerbil: Docker Compose'da Portları Açın", - "resourceLearnRaw": "TCP/UDP kaynaklarını nasıl yapılandıracağınızı öğrenin", - "resourceBack": "Kaynaklara Geri Dön", - "resourceGoTo": "Kaynağa Git", - "resourceDelete": "Kaynağı Sil", - "resourceDeleteConfirm": "Kaynak Silmeyi Onayla", - "visibility": "Görünürlük", - "enabled": "Etkin", - "disabled": "Devre Dışı", - "general": "Genel", - "generalSettings": "Genel Ayarlar", - "proxy": "Vekil Sunucu", - "internal": "Dahili", - "rules": "Kurallar", - "resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın", - "resourceSetting": "{resourceName} Ayarları", - "alwaysAllow": "Her Zaman İzin Ver", - "alwaysDeny": "Her Zaman Reddet", - "passToAuth": "Kimlik Doğrulamasına Geç", - "orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın", - "orgGeneralSettings": "Organizasyon Ayarları", - "orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin", - "saveGeneralSettings": "Genel Ayarları Kaydet", - "saveSettings": "Ayarları Kaydet", - "orgDangerZone": "Tehlike Alanı", - "orgDangerZoneDescription": "Bu organizasyonu sildikten sonra geri dönüş yoktur. Emin olun.", - "orgDelete": "Organizasyonu Sil", - "orgDeleteConfirm": "Organizasyon Silmeyi Onayla", - "orgMessageRemove": "Bu işlem geri alınamaz ve tüm ilişkili verileri silecektir.", - "orgMessageConfirm": "Onaylamak için lütfen aşağıya organizasyonun adını yazın.", - "orgQuestionRemove": "{selectedOrg} organizasyonunu kaldırmak istediğinizden emin misiniz?", - "orgUpdated": "Organizasyon güncellendi", - "orgUpdatedDescription": "Organizasyon güncellendi.", - "orgErrorUpdate": "Organizasyon güncellenemedi", - "orgErrorUpdateMessage": "Organizasyon güncellenirken bir hata oluştu.", - "orgErrorFetch": "Organizasyonlar getirilemedi", - "orgErrorFetchMessage": "Organizasyonlarınız listelenirken bir hata oluştu", - "orgErrorDelete": "Organizasyon silinemedi", - "orgErrorDeleteMessage": "Organizasyon silinirken bir hata oluştu.", - "orgDeleted": "Organizasyon silindi", - "orgDeletedMessage": "Organizasyon ve verileri silindi.", - "orgMissing": "Organizasyon Kimliği Eksik", - "orgMissingMessage": "Organizasyon kimliği olmadan daveti yeniden oluşturmanız mümkün değildir.", - "accessUsersManage": "Kullanıcıları Yönet", - "accessUsersDescription": "Kullanıcıları davet edin ve erişimi yönetmek için rollere ekleyin", - "accessUsersSearch": "Kullanıcıları ara...", - "accessUserCreate": "Kullanıcı Oluştur", - "accessUserRemove": "Kullanıcıyı Kaldır", - "username": "Kullanıcı Adı", - "identityProvider": "General Information", - "role": "Rol", - "nameRequired": "Ad gereklidir", - "accessRolesManage": "Rolleri Yönet", - "accessRolesDescription": "Organizasyonunuza erişimi yönetmek için rolleri yapılandırın", - "accessRolesSearch": "Rolleri ara...", - "accessRolesAdd": "Rol Ekle", - "accessRoleDelete": "Rolü Sil", - "description": "Açıklama", - "inviteTitle": "Açık Davetiyeler", - "inviteDescription": "Davetiyelerinizi diğer kullanıcılarla yönetin", - "inviteSearch": "Davetiyeleri ara...", - "minutes": "Dakika", - "hours": "Saat", - "days": "Gün", - "weeks": "Hafta", - "months": "Ay", - "years": "Yıl", - "day": "{count, plural, one {# gün} other {# gün}}", - "apiKeysTitle": "API Anahtar Bilgilendirmesi", - "apiKeysConfirmCopy2": "API anahtarını kopyaladığınızı onaylamanız gerekmektedir.", - "apiKeysErrorCreate": "API anahtarı oluşturulurken hata", - "apiKeysErrorSetPermission": "İzinler ayarlanırken hata", - "apiKeysCreate": "API Anahtarı Oluştur", - "apiKeysCreateDescription": "Organizasyonunuz için yeni bir API anahtarı oluşturun", - "apiKeysGeneralSettings": "İzinler", - "apiKeysGeneralSettingsDescription": "Bu API anahtarının neler yapabileceğini belirleyin", - "apiKeysList": "API Anahtarınız", - "apiKeysSave": "API Anahtarınızı Kaydedin", - "apiKeysSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", - "apiKeysInfo": "API anahtarınız:", - "apiKeysConfirmCopy": "API anahtarını kopyaladım", - "generate": "Oluştur", - "done": "Tamamlandı", - "apiKeysSeeAll": "Tüm API Anahtarlarını Gör", - "apiKeysPermissionsErrorLoadingActions": "API anahtarı eylemleri yüklenirken bir hata oluştu", - "apiKeysPermissionsErrorUpdate": "İzin ayarları sırasında bir hata oluştu", - "apiKeysPermissionsUpdated": "İzinler güncellendi", - "apiKeysPermissionsUpdatedDescription": "İzinler güncellenmiştir.", - "apiKeysPermissionsGeneralSettings": "İzinler", - "apiKeysPermissionsGeneralSettingsDescription": "Bu API anahtarının neler yapabileceğini belirleyin", - "apiKeysPermissionsSave": "İzinleri Kaydet", - "apiKeysPermissionsTitle": "İzinler", - "apiKeys": "API Anahtarları", - "searchApiKeys": "API anahtarlarını ara...", - "apiKeysAdd": "API Anahtarı Oluştur", - "apiKeysErrorDelete": "API anahtarı silinirken bir hata oluştu", - "apiKeysErrorDeleteMessage": "API anahtarı silinirken bir hata oluştu", - "apiKeysQuestionRemove": "{selectedApiKey} API anahtarını organizasyondan kaldırmak istediğinizden emin misiniz?", - "apiKeysMessageRemove": "Kaldırıldığında, API anahtarı artık kullanılamayacaktır.", - "apiKeysMessageConfirm": "Onaylamak için lütfen aşağıya API anahtarının adını yazın.", - "apiKeysDeleteConfirm": "API Anahtarının Silinmesini Onaylayın", - "apiKeysDelete": "API Anahtarını Sil", - "apiKeysManage": "API Anahtarlarını Yönet", - "apiKeysDescription": "API anahtarları entegrasyon API'sini doğrulamak için kullanılır", - "apiKeysSettings": "{apiKeyName} Ayarları", - "userTitle": "Tüm Kullanıcıları Yönet", - "userDescription": "Sistemdeki tüm kullanıcıları görün ve yönetin", - "userAbount": "Kullanıcı Yönetimi Hakkında", - "userAbountDescription": "Bu tablo sistemdeki tüm kök kullanıcı nesnelerini gösterir. Her kullanıcı birden fazla organizasyona ait olabilir. Bir kullanıcıyı bir organizasyondan kaldırmak, onların kök kullanıcı nesnesini silmez - sistemde kalmaya devam ederler. Bir kullanıcıyı sistemden tamamen kaldırmak için, bu tablodaki silme işlemini kullanarak kök kullanıcı nesnesini silmelisiniz.", - "userServer": "Sunucu Kullanıcıları", - "userSearch": "Sunucu kullanıcılarını ara...", - "userErrorDelete": "Kullanıcı silme hatası", - "userDeleteConfirm": "Kullanıcı Silinmesini Onayla", - "userDeleteServer": "Kullanıcıyı Sunucudan Sil", - "userMessageRemove": "Kullanıcı tüm organizasyonlardan çıkarılacak ve tamamen sunucudan kaldırılacaktır.", - "userMessageConfirm": "Onaylamak için lütfen aşağıya kullanıcının adını yazın.", - "userQuestionRemove": "{selectedUser} kullanıcısını sunucudan kalıcı olarak silmek istediğinizden emin misiniz?", - "licenseKey": "Lisans Anahtarı", - "valid": "Geçerli", - "numberOfSites": "Site Sayısı", - "licenseKeySearch": "Lisans anahtarlarını ara...", - "licenseKeyAdd": "Lisans Anahtarı Ekle", - "type": "Tür", - "licenseKeyRequired": "Lisans anahtarı gereklidir", - "licenseTermsAgree": "Lisans koşullarını kabul etmelisiniz", - "licenseErrorKeyLoad": "Lisans anahtarları yüklenemedi", - "licenseErrorKeyLoadDescription": "Lisans anahtarları yüklenirken bir hata oluştu.", - "licenseErrorKeyDelete": "Lisans anahtarı silinemedi", - "licenseErrorKeyDeleteDescription": "Lisans anahtarı silinirken bir hata oluştu.", - "licenseKeyDeleted": "Lisans anahtarı silindi", - "licenseKeyDeletedDescription": "Lisans anahtarı silinmiştir.", - "licenseErrorKeyActivate": "Lisans anahtarı etkinleştirilemedi", - "licenseErrorKeyActivateDescription": "Lisans anahtarı etkinleştirilirken bir hata oluştu.", - "licenseAbout": "Lisans Hakkında", - "communityEdition": "Topluluk Sürümü", - "licenseAboutDescription": "Bu, Pangolin'i ticari bir ortamda kullanan işletme ve kurumsal kullanıcılar içindir. Pangolin'i kişisel kullanım için kullanıyorsanız, bu bölümü görmezden gelebilirsiniz.", - "licenseKeyActivated": "Lisans anahtarı etkinleştirildi", - "licenseKeyActivatedDescription": "Lisans anahtarı başarıyla etkinleştirildi.", - "licenseErrorKeyRecheck": "Lisans anahtarları yeniden kontrol edilemedi", - "licenseErrorKeyRecheckDescription": "Lisans anahtarları yeniden kontrol edilirken bir hata oluştu.", - "licenseErrorKeyRechecked": "Lisans anahtarları yeniden kontrol edildi", - "licenseErrorKeyRecheckedDescription": "Tüm lisans anahtarları yeniden kontrol edilmiştir", - "licenseActivateKey": "Lisans Anahtarını Etkinleştir", - "licenseActivateKeyDescription": "Etkinleştirmek için bir lisans anahtarı girin.", - "licenseActivate": "Lisansı Etkinleştir", - "licenseAgreement": "Bu kutuyu işaretleyerek, lisans anahtarınıza bağlı olan seviye ile ilgili lisans koşullarını okuduğunuzu ve kabul ettiğinizi onaylıyorsunuz.", - "fossorialLicense": "Fossorial Ticari Lisans ve Abonelik Koşullarını Gör", - "licenseMessageRemove": "Bu, lisans anahtarını ve onun tarafından verilen tüm izinleri kaldıracaktır.", - "licenseMessageConfirm": "Onaylamak için lütfen aşağıya lisans anahtarını yazın.", - "licenseQuestionRemove": "{selectedKey} lisans anahtarını silmek istediğinizden emin misiniz?", - "licenseKeyDelete": "Lisans Anahtarını Sil", - "licenseKeyDeleteConfirm": "Lisans Anahtarının Silinmesini Onaylayın", - "licenseTitle": "Lisans Durumunu Yönet", - "licenseTitleDescription": "Sistemdeki lisans anahtarlarını görüntüleyin ve yönetin", - "licenseHost": "Ana Lisans", - "licenseHostDescription": "Ana bilgisayar için ana lisans anahtarını yönetin.", - "licensedNot": "Lisanssız", - "hostId": "Ana Bilgisayar Kimliği", - "licenseReckeckAll": "Tüm Anahtarları Yeniden Kontrol Et", - "licenseSiteUsage": "Site Kullanımı", - "licenseSiteUsageDecsription": "Bu lisansı kullanan sitelerin sayısını görüntüleyin.", - "licenseNoSiteLimit": "Lisanssız ana bilgisayar kullanan site sayısında herhangi bir sınır yoktur.", - "licensePurchase": "Lisans Satın Al", - "licensePurchaseSites": "Ek Siteler Satın Al", - "licenseSitesUsedMax": "{usedSites} / {maxSites} siteleri kullanıldı", - "licenseSitesUsed": "{count, plural, =0 {# site} one {# site} other {# site}} sistemde bulunmaktadır.", - "licensePurchaseDescription": "{selectedMode, select, license {Lisans satın almak için kaç site istediğinizi seçin. Daha sonra daha fazla site ekleyebilirsiniz.} other {mevcut lisansınıza kaç site ekleneceğini seçin.}}", - "licenseFee": "Lisans ücreti", - "licensePriceSite": "Site başına fiyat", - "total": "Toplam", - "licenseContinuePayment": "Ödemeye Devam Et", - "pricingPage": "fiyatlandırma sayfası", - "pricingPortal": "Satın Alma Portalını Gör", - "licensePricingPage": "En güncel fiyatlandırma ve indirimler için lütfen ", - "invite": "Davetiye", - "inviteRegenerate": "Daveti Tekrar Üret", - "inviteRegenerateDescription": "Önceki daveti iptal et ve yenisini oluştur", - "inviteRemove": "Daveti Kaldır", - "inviteRemoveError": "Kaldırma işlemi başarısız oldu", - "inviteRemoveErrorDescription": "Daveti kaldırırken bir hata oluştu.", - "inviteRemoved": "Davetiye kaldırıldı", - "inviteRemovedDescription": "{email} için olan davetiye kaldırıldı.", - "inviteQuestionRemove": "{email} davetini kaldırmak istediğinizden emin misiniz?", - "inviteMessageRemove": "Kaldırıldıktan sonra bu davetiye artık geçerli olmayacak. Kullanıcı tekrar davet edilebilir.", - "inviteMessageConfirm": "Onaylamak için lütfen aşağıya davetiyenin e-posta adresini yazın.", - "inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for{email, plural, ='' {}, other { for #}}? This will revoke the previous invitation.", - "inviteRemoveConfirm": "Daveti Kaldırmayı Onayla", - "inviteRegenerated": "Davetiye Yenilendi", - "inviteSent": "{email} adresine yeni bir davet gönderildi.", - "inviteSentEmail": "Kullanıcıya e-posta bildirimi gönder", - "inviteGenerate": "{email} için yeni bir davetiye oluşturuldu.", - "inviteDuplicateError": "Yinelenen Davet", - "inviteDuplicateErrorDescription": "Bu kullanıcı için zaten bir davetiye mevcut.", - "inviteRateLimitError": "Hız Sınırı Aşıldı", - "inviteRateLimitErrorDescription": "Saatte 3 yenileme sınırını aştınız. Lütfen daha sonra tekrar deneyiniz.", - "inviteRegenerateError": "Daveti Tekrar Üretme Başarısız", - "inviteRegenerateErrorDescription": "Daveti yenilerken bir hata oluştu.", - "inviteValidityPeriod": "Geçerlilik Süresi", - "inviteValidityPeriodSelect": "Geçerlilik süresini seçin", - "inviteRegenerateMessage": "Davetiye yenilendi. Kullanıcının daveti kabul etmek için aşağıdaki bağlantıya erişmesi gerekiyor.", - "inviteRegenerateButton": "Yeniden Üret", - "expiresAt": "Bitiş Tarihi", - "accessRoleUnknown": "Bilinmeyen Rol", - "placeholder": "Yer Tutucu", - "userErrorOrgRemove": "Kullanıcı kaldırma başarısız oldu", - "userErrorOrgRemoveDescription": "Kullanıcı kaldırılırken bir hata oluştu.", - "userOrgRemoved": "Kullanıcı kaldırıldı", - "userOrgRemovedDescription": "{email} kullanıcı organizasyondan kaldırılmıştır.", - "userQuestionOrgRemove": "{email} adresini organizasyondan kaldırmak istediğinizden emin misiniz?", - "userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.", - "userMessageOrgConfirm": "Onaylamak için lütfen aşağıya kullanıcının adını yazın.", - "userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla", - "userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır", - "users": "Kullanıcılar", - "accessRoleMember": "Üye", - "accessRoleOwner": "Sahip", - "userConfirmed": "Onaylandı", - "idpNameInternal": "Dahili", - "emailInvalid": "Geçersiz e-posta adresi", - "inviteValidityDuration": "Lütfen bir süre seçin", - "accessRoleSelectPlease": "Lütfen bir rol seçin", - "usernameRequired": "Kullanıcı adı gereklidir", - "idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin", - "idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.", - "accessRoleErrorFetch": "Roller alınamadı", - "accessRoleErrorFetchDescription": "Roller alınırken bir hata oluştu", - "idpErrorFetch": "Kimlik sağlayıcıları alınamadı", - "idpErrorFetchDescription": "Kimlik sağlayıcıları alınırken bir hata oluştu", - "userErrorExists": "Kullanıcı Zaten Mevcut", - "userErrorExistsDescription": "Bu kullanıcı zaten organizasyonun bir üyesidir.", - "inviteError": "Kullanıcı davet etme başarısız oldu", - "inviteErrorDescription": "Kullanıcı davet edilirken bir hata oluştu", - "userInvited": "Kullanıcı davet edildi", - "userInvitedDescription": "Kullanıcı başarıyla davet edilmiştir.", - "userErrorCreate": "Kullanıcı oluşturulamadı", - "userErrorCreateDescription": "Kullanıcı oluşturulurken bir hata oluştu", - "userCreated": "Kullanıcı oluşturuldu", - "userCreatedDescription": "Kullanıcı başarıyla oluşturulmuştur.", - "userTypeInternal": "Dahili Kullanıcı", - "userTypeInternalDescription": "Kullanıcınızı doğrudan organizasyonunuza davet edin.", - "userTypeExternal": "Harici Kullanıcı", - "userTypeExternalDescription": "Harici bir kimlik sağlayıcısıyla kullanıcı oluşturun.", - "accessUserCreateDescription": "Yeni bir kullanıcı oluşturmak için aşağıdaki adımları izleyin", - "userSeeAll": "Tüm Kullanıcıları Gör", - "userTypeTitle": "Kullanıcı Türü", - "userTypeDescription": "Kullanıcı oluşturma yöntemini belirleyin", - "userSettings": "Kullanıcı Bilgileri", - "userSettingsDescription": "Yeni kullanıcı için detayları girin", - "inviteEmailSent": "Kullanıcıya davet e-postası gönder", - "inviteValid": "Geçerli Süresi", - "selectDuration": "Süreyi seçin", - "accessRoleSelect": "Rol seçin", - "inviteEmailSentDescription": "Kullanıcıya erişim bağlantısı ile bir e-posta gönderildi. Daveti kabul etmek için bağlantıya erişmelidirler.", - "inviteSentDescription": "Kullanıcı davet edilmiştir. Daveti kabul etmek için aşağıdaki bağlantıya erişmelidirler.", - "inviteExpiresIn": "Davetiye {days, plural, one {# gün} other {# gün}} içinde sona erecektir.", - "idpTitle": "General Information", - "idpSelect": "Dış kullanıcı için kimlik sağlayıcıyı seçin", - "idpNotConfigured": "Herhangi bir kimlik sağlayıcı yapılandırılmamış. Harici kullanıcılar oluşturulmadan önce lütfen bir kimlik sağlayıcı yapılandırın.", - "usernameUniq": "Bu, seçilen kimlik sağlayıcısında bulunan benzersiz kullanıcı adıyla eşleşmelidir.", - "emailOptional": "E-posta (İsteğe Bağlı)", - "nameOptional": "İsim (İsteğe Bağlı)", - "accessControls": "Erişim Kontrolleri", - "userDescription2": "Bu kullanıcı üzerindeki ayarları yönetin", - "accessRoleErrorAdd": "Kullanıcıyı role ekleme başarısız oldu", - "accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.", - "userSaved": "Kullanıcı kaydedildi", - "userSavedDescription": "Kullanıcı güncellenmiştir.", - "autoProvisioned": "Otomatik Sağlandı", - "autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver", - "accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin", - "accessControlsSubmit": "Erişim Kontrollerini Kaydet", - "roles": "Roller", - "accessUsersRoles": "Kullanıcılar ve Roller Yönetin", - "accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyonunuza erişim yönetmek için rollere ekleyin", - "key": "Anahtar", - "createdAt": "Oluşturulma Tarihi", - "proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.", - "proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.", - "proxyEnableSSL": "SSL Etkinleştir", - "proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.", - "target": "Hedef", - "configureTarget": "Hedefleri Yapılandır", - "targetErrorFetch": "Hedefleri alamadı", - "targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu", - "siteErrorFetch": "kaynağa ulaşılamadı", - "siteErrorFetchDescription": "kaynağa ulaşılırken bir hata oluştu", - "targetErrorDuplicate": "Yinelenen hedef", - "targetErrorDuplicateDescription": "Bu ayarlarla zaten bir hedef mevcut", - "targetWireGuardErrorInvalidIp": "Geçersiz hedef IP'si", - "targetWireGuardErrorInvalidIpDescription": "Hedef IP, site alt ağında olmalıdır", - "targetsUpdated": "Hedefler Güncellendi", - "targetsUpdatedDescription": "Hedefler ve ayarlar başarıyla güncellendi", - "targetsErrorUpdate": "Hedefler güncellenemedi", - "targetsErrorUpdateDescription": "Hedefler güncellenirken bir hata oluştu", - "targetTlsUpdate": "TLS ayarları güncellendi", - "targetTlsUpdateDescription": "TLS ayarlarınız başarıyla güncellendi", - "targetErrorTlsUpdate": "TLS ayarları güncellenemedi", - "targetErrorTlsUpdateDescription": "TLS ayarlarını güncellerken bir hata oluştu", - "proxyUpdated": "Proxy ayarları güncellendi", - "proxyUpdatedDescription": "Proxy ayarlarınız başarıyla güncellenmiştir", - "proxyErrorUpdate": "Proxy ayarları güncellenemedi", - "proxyErrorUpdateDescription": "Proxy ayarlarını güncellerken bir hata oluştu", - "targetAddr": "IP / Hostname", - "targetPort": "Bağlantı Noktası", - "targetProtocol": "Protokol", - "targetTlsSettings": "HTTPS & TLS Settings", - "targetTlsSettingsDescription": "Configure TLS settings for your resource", - "targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları", - "targetTlsSni": "TLS Sunucu Adı", - "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", - "targetTlsSubmit": "Ayarları Kaydet", - "targets": "Hedefler Konfigürasyonu", - "targetsDescription": "Trafiği arka uç hizmetlerinize yönlendirmek için hedefleri ayarlayın", - "targetStickySessions": "Yapışkan Oturumları Etkinleştir", - "targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.", - "methodSelect": "Yöntemi Seç", - "targetSubmit": "Hedef Ekle", - "targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.", - "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ığı", - "proxyCustomHeaderDescription": "İstekleri proxy'lerken ayarlanacak ana bilgisayar başlığı. Varsayılanı kullanmak için boş bırakılır.", - "proxyAdditionalSubmit": "Proxy Ayarlarını Kaydet", - "subnetMaskErrorInvalid": "Geçersiz alt ağ maskesi. 0 ile 32 arasında olmalıdır.", - "ipAddressErrorInvalidFormat": "Geçersiz IP adresi formatı", - "ipAddressErrorInvalidOctet": "Geçersiz IP adresi okteti", - "path": "Yol", - "matchPath": "Yol Eşleştir", - "ipAddressRange": "IP Aralığı", - "rulesErrorFetch": "Kurallar alınamadı", - "rulesErrorFetchDescription": "Kurallar alınırken bir hata oluştu", - "rulesErrorDuplicate": "Yinelenen kural", - "rulesErrorDuplicateDescription": "Bu ayarlara sahip bir kural zaten mevcut", - "rulesErrorInvalidIpAddressRange": "Geçersiz CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "Lütfen geçerli bir CIDR değeri girin", - "rulesErrorInvalidUrl": "Geçersiz URL yolu", - "rulesErrorInvalidUrlDescription": "Lütfen geçerli bir URL yolu değeri girin", - "rulesErrorInvalidIpAddress": "Geçersiz IP", - "rulesErrorInvalidIpAddressDescription": "Lütfen geçerli bir IP adresi girin", - "rulesErrorUpdate": "Kurallar güncellenemedi", - "rulesErrorUpdateDescription": "Kurallar güncellenirken bir hata oluştu", - "rulesUpdated": "Kuralları Etkinleştir", - "rulesUpdatedDescription": "Kural değerlendirmesi güncellendi", - "rulesMatchIpAddressRangeDescription": "CIDR formatında bir adres girin (örneğin, 103.21.244.0/22)", - "rulesMatchIpAddress": "Bir IP adresi girin (örneğin, 103.21.244.12)", - "rulesMatchUrl": "Bir URL yolu veya deseni girin (örneğin, /api/v1/todos veya /api/v1/*)", - "rulesErrorInvalidPriority": "Geçersiz Öncelik", - "rulesErrorInvalidPriorityDescription": "Lütfen geçerli bir öncelik girin", - "rulesErrorDuplicatePriority": "Yinelenen Öncelikler", - "rulesErrorDuplicatePriorityDescription": "Lütfen benzersiz öncelikler girin", - "ruleUpdated": "Kurallar güncellendi", - "ruleUpdatedDescription": "Kurallar başarıyla güncellendi", - "ruleErrorUpdate": "Operasyon başarısız oldu", - "ruleErrorUpdateDescription": "Kaydetme operasyonu sırasında bir hata oluştu", - "rulesPriority": "Öncelik", - "rulesAction": "Aksiyon", - "rulesMatchType": "Eşleşme Türü", - "value": "Değer", - "rulesAbout": "Kurallar Hakkında", - "rulesAboutDescription": "Kurallar, kaynağınıza erişimi belirli bir kriterlere göre kontrol etmenizi sağlar. IP adresi veya URL yolu temelinde erişimi izin vermek veya engellemek için kurallar oluşturabilirsiniz.", - "rulesActions": "Aksiyonlar", - "rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın", - "rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz", - "rulesActionPassToAuth": "Kimlik Doğrulamasına Geç: Kimlik doğrulama yöntemlerinin denenmesine izin ver", - "rulesMatchCriteria": "Eşleşme Kriterleri", - "rulesMatchCriteriaIpAddress": "Belirli bir IP adresi ile eşleşme", - "rulesMatchCriteriaIpAddressRange": "CIDR gösteriminde bir IP adresi aralığı ile eşleşme", - "rulesMatchCriteriaUrl": "Bir URL yolu veya deseni ile eşleşme", - "rulesEnable": "Kuralları Etkinleştir", - "rulesEnableDescription": "Bu kaynak için kural değerlendirmesini etkinleştirin veya devre dışı bırakın", - "rulesResource": "Kaynak Kuralları Yapılandırması", - "rulesResourceDescription": "Kaynağınıza erişimi kontrol etmek için kuralları yapılandırın", - "ruleSubmit": "Kural Ekle", - "rulesNoOne": "Kural yok. Formu kullanarak bir kural ekleyin.", - "rulesOrder": "Kurallar, artan öncelik sırasına göre değerlendirilir.", - "rulesSubmit": "Kuralları Kaydet", - "resourceErrorCreate": "Kaynak oluşturma hatası", - "resourceErrorCreateDescription": "Kaynak oluşturulurken bir hata oluştu", - "resourceErrorCreateMessage": "Kaynak oluşturma hatası:", - "resourceErrorCreateMessageDescription": "Beklenmeyen bir hata oluştu", - "sitesErrorFetch": "Siteler alınırken hata oluştu", - "sitesErrorFetchDescription": "Siteler alınırken bir hata oluştu", - "domainsErrorFetch": "Alanlar alınırken hata oluştu", - "domainsErrorFetchDescription": "Alanlar alınırken bir hata oluştu", - "none": "Hiçbiri", - "unknown": "Bilinmiyor", - "resources": "Kaynaklar", - "resourcesDescription": "Kaynaklar, özel ağınızda çalışan uygulamalara proxy görevi görür. Özel ağınızdaki herhangi bir HTTP/HTTPS veya ham TCP/UDP hizmeti için bir kaynak oluşturun. Her kaynak, şifreli bir WireGuard tüneli aracılığıyla özel ve güvenli bağlantıyı etkinleştirmek için bir siteye bağlı olmalıdır.", - "resourcesWireGuardConnect": "WireGuard şifreleme ile güvenli bağlantı", - "resourcesMultipleAuthenticationMethods": "Birden fazla kimlik doğrulama yöntemi yapılandırın", - "resourcesUsersRolesAccess": "Kullanıcı ve rol tabanlı erişim kontrolü", - "resourcesErrorUpdate": "Kaynak değiştirilemedi", - "resourcesErrorUpdateDescription": "Kaynak güncellenirken bir hata oluştu", - "access": "Erişim", - "shareLink": "{resource} Paylaşım Bağlantısı", - "resourceSelect": "Kaynak seçin", - "shareLinks": "Paylaşım Bağlantıları", - "share": "Paylaşılabilir Bağlantılar", - "shareDescription2": "Kaynaklarınıza geçici veya sınırsız erişim sağlamak için paylaşılabilir bağlantılar oluşturun. Bağlantı oluştururken sona erme süresini yapılandırabilirsiniz.", - "shareEasyCreate": "Kolayca oluştur ve paylaş", - "shareConfigurableExpirationDuration": "Yapılandırılabilir sona erme süresi", - "shareSecureAndRevocable": "Güvenli ve iptal edilebilir", - "nameMin": "İsim en az {len} karakter olmalıdır.", - "nameMax": "İsim {len} karakterden uzun olmamalıdır.", - "sitesConfirmCopy": "Yapılandırmayı kopyaladığınızı onaylayın.", - "unknownCommand": "Bilinmeyen komut", - "newtErrorFetchReleases": "Sürüm bilgileri alınamadı: {err}", - "newtErrorFetchLatest": "Son sürüm alınırken hata: {err}", - "newtEndpoint": "Newt Uç Noktası", - "newtId": "Newt Kimliği", - "newtSecretKey": "Newt Gizli Anahtarı", - "architecture": "Mimari", - "sites": "Siteler", - "siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklarınıza eş IP adresini kullanarak erişmeniz gerekecek.", - "siteWgCompatibleAllClients": "Tüm WireGuard istemcileriyle uyumlu", - "siteWgManualConfigurationRequired": "Manuel yapılandırma gerekli", - "userErrorNotAdminOrOwner": "Kullanıcı yönetici veya sahibi değil", - "pangolinSettings": "Ayarlar - Pangolin", - "accessRoleYour": "Rolünüz:", - "accessRoleSelect2": "Bir rol seçin", - "accessUserSelect": "Bir kullanıcı seçin", - "otpEmailEnter": "Bir e-posta girin", - "otpEmailEnterDescription": "E-posta girdikten sonra girdi alanına yazıp enter'a basın.", - "otpEmailErrorInvalid": "Geçersiz e-posta adresi. Joker karakter (*) yerel kısmın tamamı olmalıdır.", - "otpEmailSmtpRequired": "SMTP Gerekli", - "otpEmailSmtpRequiredDescription": "Tek seferlik şifre kimlik doğrulamasını kullanmak için, sunucuda SMTP etkinleştirilmelidir.", - "otpEmailTitle": "Tek Seferlik Şifreler", - "otpEmailTitleDescription": "Kaynak erişimi için e-posta tabanlı kimlik doğrulamasını zorunlu kılın", - "otpEmailWhitelist": "E-posta Beyaz Listesi", - "otpEmailWhitelistList": "Beyaz Listeye Alınan E-postalar", - "otpEmailWhitelistListDescription": "Yalnızca bu e-posta adresleriyle kullanıcılar bu kaynağa erişebilecektir. E-postalarına gönderilen tek seferlik şifreyi girmeleri istenecektir. Bir etki alanından herhangi bir e-posta adresine izin vermek için joker karakterler (*@example.com) kullanılabilir.", - "otpEmailWhitelistSave": "Beyaz Listeyi Kaydet", - "passwordAdd": "Şifre Ekle", - "passwordRemove": "Şifre Kaldır", - "pincodeAdd": "PIN Kodu Ekle", - "pincodeRemove": "PIN Kodu Kaldır", - "resourceAuthMethods": "Kimlik Doğrulama Yöntemleri", - "resourceAuthMethodsDescriptions": "Ek kimlik doğrulama yöntemleriyle kaynağa erişime izin verin", - "resourceAuthSettingsSave": "Başarıyla kaydedildi", - "resourceAuthSettingsSaveDescription": "Kimlik doğrulama ayarları kaydedildi", - "resourceErrorAuthFetch": "Veriler alınamadı", - "resourceErrorAuthFetchDescription": "Veri alınırken bir hata oluştu", - "resourceErrorPasswordRemove": "Kaynak şifresi kaldırılırken hata oluştu", - "resourceErrorPasswordRemoveDescription": "Kaynak şifresi kaldırılırken bir hata oluştu", - "resourceErrorPasswordSetup": "Kaynak şifresi ayarlanırken hata oluştu", - "resourceErrorPasswordSetupDescription": "Kaynak şifresi ayarlanırken bir hata oluştu", - "resourceErrorPincodeRemove": "Kaynak pincode kaldırılırken hata oluştu", - "resourceErrorPincodeRemoveDescription": "Kaynak pincode kaldırılırken bir hata oluştu", - "resourceErrorPincodeSetup": "Kaynak PIN kodu ayarlanırken hata oluştu", - "resourceErrorPincodeSetupDescription": "Kaynak PIN kodu ayarlanırken bir hata oluştu", - "resourceErrorUsersRolesSave": "Roller kaydedilemedi", - "resourceErrorUsersRolesSaveDescription": "Roller ayarlanırken bir hata oluştu", - "resourceErrorWhitelistSave": "Beyaz liste kaydedilemedi", - "resourceErrorWhitelistSaveDescription": "Beyaz liste kaydedilirken bir hata oluştu", - "resourcePasswordSubmit": "Parola Korumasını Etkinleştir", - "resourcePasswordProtection": "Parola Koruması {status}", - "resourcePasswordRemove": "Kaynak parolası kaldırıldı", - "resourcePasswordRemoveDescription": "Kaynak parolası başarıyla kaldırıldı", - "resourcePasswordSetup": "Kaynak parolası ayarlandı", - "resourcePasswordSetupDescription": "Kaynak parolası başarıyla ayarlandı", - "resourcePasswordSetupTitle": "Parola Ayarla", - "resourcePasswordSetupTitleDescription": "Bu kaynağı korumak için bir parola ayarlayın", - "resourcePincode": "PIN Kodu", - "resourcePincodeSubmit": "PIN Kodu Korumasını Etkinleştir", - "resourcePincodeProtection": "PIN Kodu Koruması {status}", - "resourcePincodeRemove": "Kaynak pincode kaldırıldı", - "resourcePincodeRemoveDescription": "Kaynak parolası başarıyla kaldırıldı", - "resourcePincodeSetup": "Kaynak PIN kodu ayarlandı", - "resourcePincodeSetupDescription": "Kaynak pincode başarıyla ayarlandı", - "resourcePincodeSetupTitle": "Pincode Ayarla", - "resourcePincodeSetupTitleDescription": "Bu kaynağı korumak için bir pincode ayarlayın", - "resourceRoleDescription": "Yöneticiler her zaman bu kaynağa erişebilir.", - "resourceUsersRoles": "Kullanıcılar ve Roller", - "resourceUsersRolesDescription": "Bu kaynağı kimlerin ziyaret edebileceği kullanıcıları ve rolleri yapılandırın", - "resourceUsersRolesSubmit": "Kullanıcıları ve Rolleri Kaydet", - "resourceWhitelistSave": "Başarıyla kaydedildi", - "resourceWhitelistSaveDescription": "Beyaz liste ayarları kaydedildi", - "ssoUse": "Platform SSO'sunu Kullanın", - "ssoUseDescription": "Mevcut kullanıcılar yalnızca bir kez giriş yapmak zorunda kalacaktır bu etkinleştirildiğinde bütün kaynaklar için.", - "proxyErrorInvalidPort": "Geçersiz port numarası", - "subdomainErrorInvalid": "Geçersiz alt domain", - "domainErrorFetch": "Alanlar alınırken hata oluştu", - "domainErrorFetchDescription": "Alanlar alınırken bir hata oluştu", - "resourceErrorUpdate": "Kaynak güncellenemedi", - "resourceErrorUpdateDescription": "Kaynak güncellenirken bir hata oluştu", - "resourceUpdated": "Kaynak güncellendi", - "resourceUpdatedDescription": "Kaynak başarıyla güncellendi", - "resourceErrorTransfer": "Kaynak aktarılamadı", - "resourceErrorTransferDescription": "Kaynak aktarılırken bir hata oluştu", - "resourceTransferred": "Kaynak aktarıldı", - "resourceTransferredDescription": "Kaynak başarıyla aktarıldı", - "resourceErrorToggle": "Kaynak değiştirilemedi", - "resourceErrorToggleDescription": "Kaynak güncellenirken bir hata oluştu", - "resourceVisibilityTitle": "Görünürlük", - "resourceVisibilityTitleDescription": "Kaynak görünürlüğünü tamamen etkinleştirin veya devre dışı bırakın", - "resourceGeneral": "Genel Ayarlar", - "resourceGeneralDescription": "Bu kaynak için genel ayarları yapılandırın", - "resourceEnable": "Kaynağı Etkinleştir", - "resourceTransfer": "Kaynağı Aktar", - "resourceTransferDescription": "Bu kaynağı farklı bir siteye aktarın", - "resourceTransferSubmit": "Kaynağı Aktar", - "siteDestination": "Hedef Site", - "searchSites": "Siteleri ara", - "accessRoleCreate": "Rol Oluştur", - "accessRoleCreateDescription": "Kullanıcıları gruplamak ve izinlerini yönetmek için yeni bir rol oluşturun.", - "accessRoleCreateSubmit": "Rol Oluştur", - "accessRoleCreated": "Rol oluşturuldu", - "accessRoleCreatedDescription": "Rol başarıyla oluşturuldu.", - "accessRoleErrorCreate": "Rol oluşturulamadı", - "accessRoleErrorCreateDescription": "Rol oluşturulurken bir hata oluştu.", - "accessRoleErrorNewRequired": "Yeni rol gerekli", - "accessRoleErrorRemove": "Rol kaldırılamadı", - "accessRoleErrorRemoveDescription": "Rol kaldırılırken bir hata oluştu.", - "accessRoleName": "Rol Adı", - "accessRoleQuestionRemove": "{name} rolünü silmek üzeresiniz. Bu eylemi geri alamazsınız.", - "accessRoleRemove": "Rolü Kaldır", - "accessRoleRemoveDescription": "Kuruluştan bir rol kaldır", - "accessRoleRemoveSubmit": "Rolü Kaldır", - "accessRoleRemoved": "Rol kaldırıldı", - "accessRoleRemovedDescription": "Rol başarıyla kaldırıldı.", - "accessRoleRequiredRemove": "Bu rolü silmeden önce, mevcut üyeleri aktarmak için yeni bir rol seçin.", - "manage": "Yönet", - "sitesNotFound": "Site bulunamadı.", - "pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin", - "licenseTierProfessional": "Profesyonel Lisans", - "licenseTierEnterprise": "Kurumsal Lisans", - "licenseTierPersonal": "Personal License", - "licensed": "Lisanslı", - "yes": "Evet", - "no": "Hayır", - "sitesAdditional": "Ek Siteler", - "licenseKeys": "Lisans Anahtarları", - "sitestCountDecrease": "Site sayısını azalt", - "sitestCountIncrease": "Site sayısını artır", - "idpManage": "Kimlik Sağlayıcılarını Yönet", - "idpManageDescription": "Sistem içindeki kimlik sağlayıcıları görün ve yönetin", - "idpDeletedDescription": "Kimlik sağlayıcı başarıyla silindi", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "Kimlik sağlayıcıyı kalıcı olarak silmek istediğinizden emin misiniz? {name}", - "idpMessageRemove": "Bu, kimlik sağlayıcıyı ve tüm ilişkili yapılandırmaları kaldıracaktır. Bu sağlayıcıdan kimlik doğrulayan kullanıcılar artık giriş yapamayacaktır.", - "idpMessageConfirm": "Onaylamak için lütfen aşağıya kimlik sağlayıcının adını yazın.", - "idpConfirmDelete": "Kimlik Sağlayıcıyı Silme Onayı", - "idpDelete": "Kimlik Sağlayıcıyı Sil", - "idp": "Kimlik Sağlayıcıları", - "idpSearch": "Kimlik sağlayıcıları ara...", - "idpAdd": "Kimlik Sağlayıcı Ekle", - "idpClientIdRequired": "Müşteri Kimliği gereklidir.", - "idpClientSecretRequired": "Müşteri Gizli Anahtarı gereklidir.", - "idpErrorAuthUrlInvalid": "Kimlik Doğrulama URL'si geçerli bir URL olmalıdır.", - "idpErrorTokenUrlInvalid": "Token URL'si geçerli bir URL olmalıdır.", - "idpPathRequired": "Tanımlayıcı Yol gereklidir.", - "idpScopeRequired": "Kapsamlar gereklidir.", - "idpOidcDescription": "OpenID Connect kimlik sağlayıcısı yapılandırın", - "idpCreatedDescription": "Kimlik sağlayıcı başarıyla oluşturuldu", - "idpCreate": "Kimlik Sağlayıcı Oluştur", - "idpCreateDescription": "Kullanıcı kimlik doğrulaması için yeni bir kimlik sağlayıcı yapılandırın", - "idpSeeAll": "Tüm Kimlik Sağlayıcılarını Gör", - "idpSettingsDescription": "Kimlik sağlayıcınız için temel bilgileri yapılandırın", - "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": "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ı", - "idpOidcConfigureDescription": "OAuth2/OIDC sağlayıcı uç noktalarını ve kimlik bilgilerini yapılandırın", - "idpClientId": "Müşteri ID", - "idpClientIdDescription": "Kimlik sağlayıcınızdan alınan OAuth2 istemci kimliği", - "idpClientSecret": "Müşteri Gizli", - "idpClientSecretDescription": "Kimlik sağlayıcınızdan alınan OAuth2 istemci sırrı", - "idpAuthUrl": "Yetki URL'si", - "idpAuthUrlDescription": "OAuth2 yetki uç nokta URL'si", - "idpTokenUrl": "Token URL'si", - "idpTokenUrlDescription": "OAuth2 jeton uç nokta URL'si", - "idpOidcConfigureAlert": "Önemli Bilgi", - "idpOidcConfigureAlertDescription": "Kimlik sağlayıcısını oluşturduktan sonra, kimlik sağlayıcınızın ayarlarında geri arama URL'sini yapılandırmanız gerekecektir. Geri arama URL'si başarılı bir oluşturma işleminden sonra sağlanacaktır.", - "idpToken": "Token Yapılandırma", - "idpTokenDescription": "Kullanıcı bilgisini ID token'dan nasıl çıkaracağınızı yapılandırın", - "idpJmespathAbout": "JMESPath Hakkında", - "idpJmespathAboutDescription": "Aşağıdaki yollar, ID token'dan değerleri çıkarmak için JMESPath sözdizimini kullanır.", - "idpJmespathAboutDescriptionLink": "JMESPath hakkında daha fazla bilgi edinin", - "idpJmespathLabel": "Tanımlayıcı Yolu", - "idpJmespathLabelDescription": "The JMESPath to the user identifier in the ID token", - "idpJmespathEmailPathOptional": "E-posta Yolu (İsteğe Bağlı)", - "idpJmespathEmailPathOptionalDescription": "The JMESPath to the user's email in the ID token", - "idpJmespathNamePathOptional": "Ad Yolu (İsteğe Bağlı)", - "idpJmespathNamePathOptionalDescription": "The JMESPath to the user's name in the ID token", - "idpOidcConfigureScopes": "Kapsamlar", - "idpOidcConfigureScopesDescription": "Talep edilecek OAuth2 kapsamlarının boşlukla ayrılmış listesi", - "idpSubmit": "Kimlik Sağlayıcı Oluştur", - "orgPolicies": "Kuruluş Politikaları", - "idpSettings": "{idpName} Ayarları", - "idpCreateSettingsDescription": "Kimlik sağlayıcınız için ayarları yapılandırın", - "roleMapping": "Rol Eşlemesi", - "orgMapping": "Kuruluş Eşlemesi", - "orgPoliciesSearch": "Kuruluş politikalarını ara...", - "orgPoliciesAdd": "Kuruluş Politikası Ekle", - "orgRequired": "Kuruluş gereklidir", - "error": "Hata", - "success": "Başarı", - "orgPolicyAddedDescription": "Politika başarıyla eklendi", - "orgPolicyUpdatedDescription": "Politika başarıyla güncellendi", - "orgPolicyDeletedDescription": "Politika başarıyla silindi", - "defaultMappingsUpdatedDescription": "Varsayılan eşlemeler başarıyla güncellendi", - "orgPoliciesAbout": "Kuruluş Politikaları Hakkında", - "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. For more information, see", - "orgPoliciesAboutDescriptionLink": "the documentation", - "defaultMappingsOptional": "Varsayılan Eşlemeler (İsteğe Bağlı)", - "defaultMappingsOptionalDescription": "Varsayılan eşlemeler, bir kuruluş için bir kuruluş politikası tanımlı olmadığında kullanılır. Burada varsayılan rol ve kuruluş eşlemelerini belirtebilirsiniz.", - "defaultMappingsRole": "Varsayılan Rol Eşleme", - "defaultMappingsRoleDescription": "JMESPath to extract role information from the ID token. The result of this expression must return the role name as defined in the organization as a string.", - "defaultMappingsOrg": "Varsayılan Kuruluş Eşleme", - "defaultMappingsOrgDescription": "JMESPath to extract organization information from the ID token. This expression must return the org ID or true for the user to be allowed to access the organization.", - "defaultMappingsSubmit": "Varsayılan Eşlemeleri Kaydet", - "orgPoliciesEdit": "Kuruluş Politikasını Düzenle", - "org": "Kuruluş", - "orgSelect": "Kuruluşu seç", - "orgSearch": "Kuruluşu ara", - "orgNotFound": "Kuruluş bulunamadı.", - "roleMappingPathOptional": "Rol Eşleme Yolu (İsteğe Bağlı)", - "orgMappingPathOptional": "Kuruluş Eşleme Yolu (İsteğe Bağlı)", - "orgPolicyUpdate": "Politikayı Güncelle", - "orgPolicyAdd": "Politika Ekle", - "orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın", - "idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi", - "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": "Yetkilendirme - Pangolin", - "verificationCodeLengthRequirements": "Doğrulama kodunuz 8 karakter olmalıdır.", - "errorOccurred": "Bir hata oluştu", - "emailErrorVerify": "E-posta doğrulanamadı: ", - "emailVerified": "E-posta başarıyla doğrulandı! Yönlendiriliyorsunuz...", - "verificationCodeErrorResend": "Doğrulama kodu yeniden gönderilemedi:", - "verificationCodeResend": "Doğrulama kodu yeniden gönderildi", - "verificationCodeResendDescription": "E-posta adresinize bir doğrulama kodu yeniden gönderdik. Lütfen gelen kutunuzu kontrol edin.", - "emailVerify": "E-posta Onayla", - "emailVerifyDescription": "E-posta adresinize gönderilen doğrulama kodunu girin.", - "verificationCode": "Doğrulama Kodu", - "verificationCodeEmailSent": "E-posta adresinize bir doğrulama kodu gönderdik.", - "submit": "Gönder", - "emailVerifyResendProgress": "Yeniden gönderiliyor...", - "emailVerifyResend": "Kod gelmedi mi? Tekrar göndermek için buraya tıklayın", - "passwordNotMatch": "Parolalar eşleşmiyor", - "signupError": "Kaydolurken bir hata oluştu", - "pangolinLogoAlt": "Pangolin Logosu", - "inviteAlready": "Davetiye gönderilmiş gibi görünüyor!", - "inviteAlreadyDescription": "Daveti kabul etmek için giriş yapmalı veya bir hesap oluşturmalısınız.", - "signupQuestion": "Zaten bir hesabınız var mı?", - "login": "Giriş yap", - "resourceNotFound": "No resources found", - "resourceNotFoundDescription": "Erişmeye çalıştığınız kaynak mevcut değil.", - "pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır", - "pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır", - "passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır", - "passwordRequirementsTitle": "Şifre gereksinimleri:", - "passwordRequirementLength": "En az 8 karakter uzunluğunda", - "passwordRequirementUppercase": "En az bir büyük harf", - "passwordRequirementLowercase": "En az bir küçük harf", - "passwordRequirementNumber": "En az bir sayı", - "passwordRequirementSpecial": "En az bir özel karakter", - "passwordRequirementsMet": "✓ Şifre tüm gereksinimleri karşılıyor", - "passwordStrength": "Şifre gücü", - "passwordStrengthWeak": "Zayıf", - "passwordStrengthMedium": "Orta", - "passwordStrengthStrong": "Güçlü", - "passwordRequirements": "Gereksinimler:", - "passwordRequirementLengthText": "8+ karakter", - "passwordRequirementUppercaseText": "Büyük harf (A-Z)", - "passwordRequirementLowercaseText": "Küçük harf (a-z)", - "passwordRequirementNumberText": "Sayı (0-9)", - "passwordRequirementSpecialText": "Özel karakter (!@#$%...)", - "passwordsDoNotMatch": "Parolalar eşleşmiyor", - "otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır", - "otpEmailSent": "OTP Gönderildi", - "otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi", - "otpEmailErrorAuthenticate": "E-posta ile kimlik doğrulama başarasız oldu", - "pincodeErrorAuthenticate": "PIN kodu ile kimlik doğrulama başarısız oldu", - "passwordErrorAuthenticate": "Şifre ile kimlik doğrulama başarısız oldu", - "poweredBy": "Tarafından sağlanmıştır", - "authenticationRequired": "Kimlik Doğrulama Gerekiyor", - "authenticationMethodChoose": "{name} erişimi için tercih edilen yöntemi seçin", - "authenticationRequest": "{name} erişimi için kimlik doğrulamanız gerekiyor", - "user": "Kullanıcı", - "pincodeInput": "6 haneli PIN Kodu", - "pincodeSubmit": "PIN ile Giriş Yap", - "passwordSubmit": "Şifre ile Giriş Yap", - "otpEmailDescription": "Bu e-posta adresine tek kullanımlık bir kod gönderilecektir.", - "otpEmailSend": "Tek Kullanımlık Kod Gönder", - "otpEmail": "Tek Kullanımlık Parola (OTP)", - "otpEmailSubmit": "OTP Gönder", - "backToEmail": "E-postaya Geri Dön", - "noSupportKey": "Sunucu destek anahtarı olmadan çalışıyor. Projeyi desteklemeyi düşünün!", - "accessDenied": "Erişim Reddedildi", - "accessDeniedDescription": "Bu kaynağa erişim izniniz yok. Bunun bir hata olduğunu düşünüyorsanız lütfen yöneticiyle iletişime geçin.", - "accessTokenError": "Erişim jetonu kontrol ederken hata oluştu", - "accessGranted": "Erişim İzni Verildi", - "accessUrlInvalid": "Erişim URL'si Geçersiz", - "accessGrantedDescription": "Bu kaynağa erişim izni verildi. Yönlendiriliyorsunuz...", - "accessUrlInvalidDescription": "Bu paylaşılan erişim URL'si geçersiz. Yeni bir URL için lütfen kaynak sahibine başvurun.", - "tokenInvalid": "Geçersiz jeton", - "pincodeInvalid": "Geçersiz kod", - "passwordErrorRequestReset": "Şifre sıfırlama isteği başarısız oldu:", - "passwordErrorReset": "Şifre sıfırlama başarısız oldu:", - "passwordResetSuccess": "Şifre başarıyla sıfırlandı! Girişe geri...", - "passwordReset": "Şifreyi Yenile", - "passwordResetDescription": "Şifrenizi sıfırlamak için adımları uygulayın", - "passwordResetSent": "Bu e-posta adresine bir şifre sıfırlama kodu gönderilecektir.", - "passwordResetCode": "Sıfırlama Kodu", - "passwordResetCodeDescription": "E-posta gelen kutunuzda sıfırlama kodunu kontrol edin.", - "passwordNew": "Yeni Şifre", - "passwordNewConfirm": "Yeni Şifreyi Onayla", - "pincodeAuth": "Kimlik Doğrulama Kodu", - "pincodeSubmit2": "Kodu Gönder", - "passwordResetSubmit": "Sıfırlama İsteği", - "passwordBack": "Şifreye Geri Dön", - "loginBack": "Girişe geri dön", - "signup": "Kaydol", - "loginStart": "Başlamak için giriş yapın", - "idpOidcTokenValidating": "OIDC token'ı doğrulanıyor", - "idpOidcTokenResponse": "OIDC token yanıtını doğrula", - "idpErrorOidcTokenValidating": "OIDC token'ı doğrularken hata", - "idpConnectingTo": "{name} ile bağlantı kuruluyor", - "idpConnectingToDescription": "Kimliğiniz doğrulanıyor", - "idpConnectingToProcess": "Bağlanılıyor...", - "idpConnectingToFinished": "Bağlandı", - "idpErrorConnectingTo": "{name} ile bağlantı kurarken bir sorun meydana geldi. Lütfen yöneticiye danışın.", - "idpErrorNotFound": "IdP bulunamadı", - "inviteInvalid": "Geçersiz Davet", - "inviteInvalidDescription": "Davet bağlantısı geçersiz.", - "inviteErrorWrongUser": "Davet bu kullanıcı için değil", - "inviteErrorUserNotExists": "Kullanıcı mevcut değil. Lütfen önce bir hesap oluşturun.", - "inviteErrorLoginRequired": "Bir daveti kabul etmek için giriş yapmış olmanız gerekir", - "inviteErrorExpired": "Davet süresi dolmuş olabilir", - "inviteErrorRevoked": "Davet iptal edilmiş olabilir", - "inviteErrorTypo": "Davet bağlantısında yazım hatası olabilir", - "pangolinSetup": "Kurulum - Pangolin", - "orgNameRequired": "Kuruluş adı gereklidir", - "orgIdRequired": "Kuruluş ID gereklidir", - "orgErrorCreate": "Kuruluş oluşturulurken bir hata oluştu", - "pageNotFound": "Sayfa Bulunamadı", - "pageNotFoundDescription": "Oops! Aradığınız sayfa mevcut değil.", - "overview": "Genel Bakış", - "home": "Ana Sayfa", - "accessControl": "Erişim Kontrolü", - "settings": "Ayarlar", - "usersAll": "Tüm Kullanıcılar", - "license": "Lisans", - "pangolinDashboard": "Kontrol Paneli - Pangolin", - "noResults": "Sonuç bulunamadı.", - "terabytes": "{count} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "Girilen Etiketler", - "tagsEnteredDescription": "Bunlar girilen etiketlerdir.", - "tagsWarnCannotBeLessThanZero": "maxTags ve minTags 0'ın altında olamaz", - "tagsWarnNotAllowedAutocompleteOptions": "Otomatik tamamlama seçeneklerine göre etiket izin verilmiyor", - "tagsWarnInvalid": "validateTag'e göre geçersiz etiket", - "tagWarnTooShort": "Etiket {tagText} çok kısa", - "tagWarnTooLong": "Etiket {tagText} çok uzun", - "tagsWarnReachedMaxNumber": "İzin verilen maksimum etiket sayısına ulaşıldı", - "tagWarnDuplicate": "Yinelenen etiket {tagText} eklenmedi", - "supportKeyInvalid": "Geçersiz Anahtar", - "supportKeyInvalidDescription": "Destekleyici anahtarınız geçersiz.", - "supportKeyValid": "Geçerli Anahtar", - "supportKeyValidDescription": "Destekleyici anahtarınız doğrulandı. Desteğiniz için teşekkürler!", - "supportKeyErrorValidationDescription": "Destekleyici anahtar doğrulanamadı.", - "supportKey": "Geliştirmeyi Destekleyin ve Bir Pangolin Edinin!", - "supportKeyDescription": "Pangolin uygulamasını topluluk için geliştirmemize devam etmemize yardımcı olacak bir destek anahtarı satın alın. Katkınız, herkese uygulamanın bakımını yapmamıza ve yeni özellikler eklememize daha fazla zaman ayırmamıza olanak tanır. Bu özellikleri ücretli hale getirmek için kullanılmayacaktır. Bu durum Ticari Sürümden tamamen ayrıdır.", - "supportKeyPet": "Ayrıca kendi evcil Pangolininize sahip olacak ve onunla tanışacaksınız!", - "supportKeyPurchase": "Ödemeler GitHub üzerinden işlenir. Daha sonra anahtarınızı şu yerden alabilirsiniz:", - "supportKeyPurchaseLink": "web sitemiz", - "supportKeyPurchase2": "ve burada kullanabilirsiniz.", - "supportKeyLearnMore": "Daha fazla bilgi.", - "supportKeyOptions": "Size en uygun seçeneği lütfen seçin.", - "supportKetOptionFull": "Tam Destek", - "forWholeServer": "Tüm sunucu için", - "lifetimePurchase": "Ömür Boyu satın alma", - "supporterStatus": "Destekçi durumu", - "buy": "Satın Al", - "supportKeyOptionLimited": "Sınırlı Destek", - "forFiveUsers": "5 veya daha az kullanıcı için", - "supportKeyRedeem": "Destekleyici Anahtarı Gir", - "supportKeyHideSevenDays": "7 gün boyunca gizle", - "supportKeyEnter": "Destekçi Anahtarını Gir", - "supportKeyEnterDescription": "Kendi evcil Pangolininle tanış!", - "githubUsername": "GitHub Kullanıcı Adı", - "supportKeyInput": "Destekçi Anahtarı", - "supportKeyBuy": "Destekçi Anahtarı Satın Al", - "logoutError": "Çıkış yaparken hata", - "signingAs": "Olarak giriş yapıldı", - "serverAdmin": "Sunucu Yöneticisi", - "managedSelfhosted": "Yönetilen Self-Hosted", - "otpEnable": "İki faktörlü özelliğini etkinleştir", - "otpDisable": "İki faktörlü özelliğini devre dışı bırak", - "logout": "Çıkış Yap", - "licenseTierProfessionalRequired": "Profesyonel Sürüme Gereklidir", - "licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.", - "actionGetOrg": "Kuruluşu Al", - "updateOrgUser": "Organizasyon Kullanıcısını Güncelle", - "createOrgUser": "Organizasyon Kullanıcısı Oluştur", - "actionUpdateOrg": "Kuruluşu Güncelle", - "actionUpdateUser": "Kullanıcıyı Güncelle", - "actionGetUser": "Kullanıcıyı Getir", - "actionGetOrgUser": "Kuruluş Kullanıcısını Al", - "actionListOrgDomains": "Kuruluş Alan Adlarını Listele", - "actionCreateSite": "Site Oluştur", - "actionDeleteSite": "Siteyi Sil", - "actionGetSite": "Siteyi Al", - "actionListSites": "Siteleri Listele", - "actionApplyBlueprint": "Planı Uygula", - "setupToken": "Kurulum Simgesi", - "setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.", - "setupTokenRequired": "Kurulum simgesi gerekli", - "actionUpdateSite": "Siteyi Güncelle", - "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", - "actionCreateResource": "Kaynak Oluştur", - "actionDeleteResource": "Kaynağı Sil", - "actionGetResource": "Kaynağı Al", - "actionListResource": "Kaynakları Listele", - "actionUpdateResource": "Kaynağı Güncelle", - "actionListResourceUsers": "Kaynak Kullanıcılarını Listele", - "actionSetResourceUsers": "Kaynak Kullanıcılarını Ayarla", - "actionSetAllowedResourceRoles": "İzin Verilen Kaynak Rolleri Ayarla", - "actionListAllowedResourceRoles": "İzin Verilen Kaynak Rolleri Listele", - "actionSetResourcePassword": "Kaynak Şifresini Ayarla", - "actionSetResourcePincode": "Kaynak PIN Kodunu Ayarla", - "actionSetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesi Ayarla", - "actionGetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesini Al", - "actionCreateTarget": "Hedef Oluştur", - "actionDeleteTarget": "Hedefi Sil", - "actionGetTarget": "Hedefi Al", - "actionListTargets": "Hedefleri Listele", - "actionUpdateTarget": "Hedefi Güncelle", - "actionCreateRole": "Rol Oluştur", - "actionDeleteRole": "Rolü Sil", - "actionGetRole": "Rolü Al", - "actionListRole": "Rolleri Listele", - "actionUpdateRole": "Rolü Güncelle", - "actionListAllowedRoleResources": "İzin Verilen Rol Kaynakları Listele", - "actionInviteUser": "Kullanıcıyı Davet Et", - "actionRemoveUser": "Kullanıcıyı Kaldır", - "actionListUsers": "Kullanıcıları Listele", - "actionAddUserRole": "Kullanıcı Rolü Ekle", - "actionGenerateAccessToken": "Erişim Jetonu Oluştur", - "actionDeleteAccessToken": "Erişim Jetonunu Sil", - "actionListAccessTokens": "Erişim Jetonlarını Listele", - "actionCreateResourceRule": "Kaynak Kuralı Oluştur", - "actionDeleteResourceRule": "Kaynak Kuralını Sil", - "actionListResourceRules": "Kaynak Kurallarını Listele", - "actionUpdateResourceRule": "Kaynak Kuralını Güncelle", - "actionListOrgs": "Organizasyonları Listele", - "actionCheckOrgId": "Kimliği Kontrol Et", - "actionCreateOrg": "Organizasyon Oluştur", - "actionDeleteOrg": "Organizasyonu Sil", - "actionListApiKeys": "API Anahtarlarını Listele", - "actionListApiKeyActions": "API Anahtarı İşlemlerini Listele", - "actionSetApiKeyActions": "API Anahtarı İzin Verilen İşlemleri Ayarla", - "actionCreateApiKey": "API Anahtarı Oluştur", - "actionDeleteApiKey": "API Anahtarını Sil", - "actionCreateIdp": "Kimlik Sağlayıcı Oluştur", - "actionUpdateIdp": "Kimlik Sağlayıcıyı Güncelle", - "actionDeleteIdp": "Kimlik Sağlayıcıyı Sil", - "actionListIdps": "Kimlik Sağlayıcı Listesi", - "actionGetIdp": "Kimlik Sağlayıcıyı Getir", - "actionCreateIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Oluştur", - "actionDeleteIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Sil", - "actionListIdpOrgs": "Kimlik Sağlayıcı Organizasyonları Listele", - "actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle", - "actionCreateClient": "Müşteri Oluştur", - "actionDeleteClient": "Müşteri Sil", - "actionUpdateClient": "Müşteri Güncelle", - "actionListClients": "Müşterileri Listele", - "actionGetClient": "Müşteriyi Al", - "actionCreateSiteResource": "Site Kaynağı Oluştur", - "actionDeleteSiteResource": "Site Kaynağını Sil", - "actionGetSiteResource": "Site Kaynağını Al", - "actionListSiteResources": "Site Kaynaklarını Listele", - "actionUpdateSiteResource": "Site Kaynağını Güncelle", - "actionListInvitations": "Davetiyeleri Listele", - "noneSelected": "Hiçbiri seçili değil", - "orgNotFound2": "Hiçbir organizasyon bulunamadı.", - "searchProgress": "Ara...", - "create": "Oluştur", - "orgs": "Organizasyonlar", - "loginError": "Giriş yaparken bir hata oluştu", - "passwordForgot": "Şifrenizi mi unuttunuz?", - "otpAuth": "İki Faktörlü Kimlik Doğrulama", - "otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.", - "otpAuthSubmit": "Kodu Gönder", - "idpContinue": "Veya devam et:", - "otpAuthBack": "Girişe Dön", - "navbar": "Navigasyon Menüsü", - "navbarDescription": "Uygulamanın ana navigasyon menüsü", - "navbarDocsLink": "Dokümantasyon", - "otpErrorEnable": "2FA etkinleştirilemedi", - "otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu", - "otpSetupCheckCode": "6 haneli bir kod girin", - "otpSetupCheckCodeRetry": "Geçersiz kod. Lütfen tekrar deneyin.", - "otpSetup": "İki Faktörlü Kimlik Doğrulamayı Etkinleştir", - "otpSetupDescription": "Hesabınızı ekstra bir koruma katmanıyla güvence altına alın", - "otpSetupScanQr": "Authenticator uygulamanızla bu QR kodunu tarayın veya gizli anahtarı manuel olarak girin:", - "otpSetupSecretCode": "Kimlik Doğrulayıcı Kodu", - "otpSetupSuccess": "İki Faktörlü Kimlik Doğrulama Etkinleştirildi", - "otpSetupSuccessStoreBackupCodes": "Hesabınız artık daha güvenli. Yedek kodlarınızı kaydetmeyi unutmayın.", - "otpErrorDisable": "2FA devre dışı bırakılamadı", - "otpErrorDisableDescription": "2FA devre dışı bırakılırken bir hata oluştu", - "otpRemove": "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak", - "otpRemoveDescription": "Hesabınız için iki faktörlü kimlik doğrulamayı devre dışı bırakın", - "otpRemoveSuccess": "İki Faktörlü Kimlik Doğrulama Devre Dışı", - "otpRemoveSuccessMessage": "Hesabınız için iki faktörlü kimlik doğrulama devre dışı bırakıldı. İstediğiniz zaman tekrar etkinleştirebilirsiniz.", - "otpRemoveSubmit": "2FA'yı Devre Dışı Bırak", - "paginator": "Sayfa {current} / {last}", - "paginatorToFirst": "İlk sayfaya git", - "paginatorToPrevious": "Önceki sayfaya git", - "paginatorToNext": "Sonraki sayfaya git", - "paginatorToLast": "Son sayfaya git", - "copyText": "Metni kopyala", - "copyTextFailed": "Metin kopyalanamadı: ", - "copyTextClipboard": "Panoya kopyala", - "inviteErrorInvalidConfirmation": "Geçersiz onay", - "passwordRequired": "Şifre gerekli", - "allowAll": "Tümüne İzin Ver", - "permissionsAllowAll": "Tüm İzinlere İzin Ver", - "githubUsernameRequired": "GitHub kullanıcı adı gereklidir", - "supportKeyRequired": "Destekleyici anahtar gereklidir", - "passwordRequirementsChars": "Şifre en az 8 karakter olmalıdır", - "language": "Dil", - "verificationCodeRequired": "Kod gerekli", - "userErrorNoUpdate": "Güncellenecek kullanıcı yok", - "siteErrorNoUpdate": "Güncellenecek site yok", - "resourceErrorNoUpdate": "Güncellenecek kaynak yok", - "authErrorNoUpdate": "Güncellenecek kimlik doğrulama bilgisi yok", - "orgErrorNoUpdate": "Güncellenecek organizasyon yok", - "orgErrorNoProvided": "Sağlanan organizasyon yok", - "apiKeysErrorNoUpdate": "Güncellenecek API anahtarı yok", - "sidebarOverview": "Genel Bakış", - "sidebarHome": "Ana Sayfa", - "sidebarSites": "Siteler", - "sidebarResources": "Kaynaklar", - "sidebarAccessControl": "Erişim Kontrolü", - "sidebarUsers": "Kullanıcılar", - "sidebarInvitations": "Davetiye", - "sidebarRoles": "Roller", - "sidebarShareableLinks": "Paylaşılabilir Bağlantılar", - "sidebarApiKeys": "API Anahtarları", - "sidebarSettings": "Ayarlar", - "sidebarAllUsers": "Tüm Kullanıcılar", - "sidebarIdentityProviders": "Kimlik Sağlayıcılar", - "sidebarLicense": "Lisans", - "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.", - "enableDockerSocketLink": "Daha fazla bilgi", - "viewDockerContainers": "Docker Konteynerlerini Görüntüle", - "containersIn": "{siteName} içindeki konteynerler", - "selectContainerDescription": "Bu hedef için bir ana bilgisayar adı olarak kullanmak üzere herhangi bir konteyner seçin. Bir bağlantı noktası kullanmak için bir bağlantı noktasına tıklayın.", - "containerName": "Ad", - "containerImage": "Görsel", - "containerState": "Durum", - "containerNetworks": "Ağlar", - "containerHostnameIp": "Ana Makine/IP", - "containerLabels": "Etiketler", - "containerLabelsCount": "{count, plural, one {# etiket} other {# etiketler}}", - "containerLabelsTitle": "Konteyner Etiketleri", - "containerLabelEmpty": "", - "containerPorts": "Bağlantı Noktaları", - "containerPortsMore": "+{count} tane daha", - "containerActions": "İşlemler", - "select": "Seç", - "noContainersMatchingFilters": "Mevcut filtrelerle uyuşan konteyner bulunamadı.", - "showContainersWithoutPorts": "Bağlantı noktası olmayan konteynerleri göster", - "showStoppedContainers": "Durdurulmuş konteynerleri göster", - "noContainersFound": "Konteyner bulunamadı. Docker konteynerlerinin çalıştığından emin olun.", - "searchContainersPlaceholder": "{count} konteyner arasında arama yapın...", - "searchResultsCount": "{count, plural, one {# sonuç} other {# sonuçlar}}", - "filters": "Filtreler", - "filterOptions": "Filtre Seçenekleri", - "filterPorts": "Bağlantı Noktaları", - "filterStopped": "Durdurulanlar", - "clearAllFilters": "Tüm filtreleri temizle", - "columns": "Sütunlar", - "toggleColumns": "Sütunları Aç/Kapat", - "refreshContainersList": "Konteyner listesi yenile", - "searching": "Aranıyor...", - "noContainersFoundMatching": "\"{filter}\" ile eşleşen konteyner bulunamadı.", - "light": "açık", - "dark": "koyu", - "system": "sistem", - "theme": "Tema", - "subnetRequired": "Alt ağ gereklidir", - "initialSetupTitle": "İlk Sunucu Kurulumu", - "initialSetupDescription": "İlk sunucu yönetici hesabını oluşturun. Yalnızca bir sunucu yöneticisi olabilir. Bu kimlik bilgilerini daha sonra her zaman değiştirebilirsiniz.", - "createAdminAccount": "Yönetici Hesabı Oluştur", - "setupErrorCreateAdmin": "Sunucu yönetici hesabı oluşturulurken bir hata oluştu.", - "certificateStatus": "Sertifika Durumu", - "loading": "Yükleniyor", - "restart": "Yeniden Başlat", - "domains": "Alan Adları", - "domainsDescription": "Organizasyonunuz için alan adlarını yönetin", - "domainsSearch": "Alan adlarını ara...", - "domainAdd": "Alan Adı Ekle", - "domainAddDescription": "Organizasyonunuz için yeni bir alan adı kaydedin", - "domainCreate": "Alan Adı Oluştur", - "domainCreatedDescription": "Alan adı başarıyla oluşturuldu", - "domainDeletedDescription": "Alan adı başarıyla silindi", - "domainQuestionRemove": "{domain} alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?", - "domainMessageRemove": "Kaldırıldığında, alan adı hesabınızla ilişkilendirilmeyecek.", - "domainMessageConfirm": "Onaylamak için lütfen aşağıya alan adını yazın.", - "domainConfirmDelete": "Alan Adı Silinmesini Onayla", - "domainDelete": "Alan Adını Sil", - "domain": "Alan Adı", - "selectDomainTypeNsName": "Alan Adı Delege Etme (NS)", - "selectDomainTypeNsDescription": "Bu alan adı ve tüm alt alan adları. Tüm bir alan adı bölgesini kontrol etmek istediğinizde bunu kullanın.", - "selectDomainTypeCnameName": "Tekil Alan Adı (CNAME)", - "selectDomainTypeCnameDescription": "Sadece bu belirli alan adı. Bireysel alt alan adları veya belirli alan adı girişleri için bunu kullanın.", - "selectDomainTypeWildcardName": "Wildcard Alan Adı", - "selectDomainTypeWildcardDescription": "Bu domain ve alt alan adları.", - "domainDelegation": "Tekil Alan Adı", - "selectType": "Bir tür seçin", - "actions": "İşlemler", - "refresh": "Yenile", - "refreshError": "Veriler yenilenemedi", - "verified": "Doğrulandı", - "pending": "Beklemede", - "sidebarBilling": "Faturalama", - "billing": "Faturalama", - "orgBillingDescription": "Fatura bilgilerinizi ve aboneliklerinizi yönetin", - "github": "GitHub", - "pangolinHosted": "Pangolin Barındırılan", - "fossorial": "Fossorial", - "completeAccountSetup": "Hesap Kurulumunu Tamamla", - "completeAccountSetupDescription": "Başlamak için şifrenizi ayarlayın", - "accountSetupSent": "Bu e-posta adresine bir hesap kurulum kodu göndereceğiz.", - "accountSetupCode": "Kurulum Kodu", - "accountSetupCodeDescription": "Kurulum kodu için e-posta gelen kutunuzu kontrol edin.", - "passwordCreate": "Parola Oluştur", - "passwordCreateConfirm": "Şifreyi Onayla", - "accountSetupSubmit": "Kurulum Kodunu Gönder", - "completeSetup": "Kurulumu Tamamla", - "accountSetupSuccess": "Hesap kurulumu tamamlandı! Pangolin'e hoş geldiniz!", - "documentation": "Dokümantasyon", - "saveAllSettings": "Tüm Ayarları Kaydet", - "settingsUpdated": "Ayarlar güncellendi", - "settingsUpdatedDescription": "Tüm ayarlar başarıyla güncellendi", - "settingsErrorUpdate": "Ayarlar güncellenemedi", - "settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu", - "sidebarCollapse": "Daralt", - "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": "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", - "domainPickerTabAll": "Tümü", - "domainPickerTabOrganization": "Organizasyon", - "domainPickerTabProvided": "Sağlanan", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...", - "domainPickerNoMatchingDomains": "Eşleşen domain bulunamadı. Farklı bir domain deneyin veya organizasyonunuzun domain ayarlarını kontrol edin.", - "domainPickerOrganizationDomains": "Organizasyon Alan Adları", - "domainPickerProvidedDomains": "Sağlanan Alan Adları", - "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", - "createNewOrgDescription": "Yeni bir organizasyon oluşturun", - "organization": "Kuruluş", - "port": "Bağlantı Noktası", - "securityKeyManage": "Güvenlik Anahtarlarını Yönet", - "securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın", - "securityKeyRegister": "Yeni Güvenlik Anahtarı Kaydet", - "securityKeyList": "Güvenlik Anahtarlarınız", - "securityKeyNone": "Henüz kayıtlı güvenlik anahtarı yok", - "securityKeyNameRequired": "İsim gerekli", - "securityKeyRemove": "Kaldır", - "securityKeyLastUsed": "Son kullanım: {date}", - "securityKeyNameLabel": "İsim", - "securityKeyRegisterSuccess": "Güvenlik anahtarı başarıyla kaydedildi", - "securityKeyRegisterError": "Güvenlik anahtarı kaydedilirken hata oluştu", - "securityKeyRemoveSuccess": "Güvenlik anahtarı başarıyla kaldırıldı", - "securityKeyRemoveError": "Güvenlik anahtarı kaldırılırken hata oluştu", - "securityKeyLoadError": "Güvenlik anahtarları yüklenirken hata oluştu", - "securityKeyLogin": "Güvenlik anahtarı ile devam edin", - "securityKeyAuthError": "Güvenlik anahtarı ile kimlik doğrulama başarısız oldu", - "securityKeyRecommendation": "Hesabınızdan kilitlenmediğinizden emin olmak için farklı bir cihazda başka bir güvenlik anahtarı kaydetmeyi düşünün.", - "registering": "Kaydediliyor...", - "securityKeyPrompt": "Lütfen güvenlik anahtarınızı kullanarak kimliğinizi doğrulayın. Güvenlik anahtarınızın bağlı ve hazır olduğundan emin olun.", - "securityKeyBrowserNotSupported": "Tarayıcınız güvenlik anahtarlarını desteklemiyor. Lütfen Chrome, Firefox veya Safari gibi modern bir tarayıcı kullanın.", - "securityKeyPermissionDenied": "Giriş yapmaya devam etmek için lütfen güvenlik anahtarınıza erişime izin verin.", - "securityKeyRemovedTooQuickly": "Güvenlik anahtarınızın bağlantısını kesmeden önce oturum açma işlemi tamamlanana kadar bağlı kalmasını sağlayın.", - "securityKeyNotSupported": "Güvenlik anahtarınız uyumlu olmayabilir. Lütfen farklı bir güvenlik anahtarı deneyin.", - "securityKeyUnknownError": "Güvenlik anahtarınızı kullanırken bir sorun oluştu. Lütfen tekrar deneyin.", - "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.", - "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", - "securityKeyTwoFactorRequired": "İki Faktörlü Kimlik Doğrulama Gereklidir", - "securityKeyTwoFactorDescription": "Güvenlik anahtarını kaydetmek için lütfen iki faktörlü kimlik doğrulama kodunuzu girin", - "securityKeyTwoFactorRemoveDescription": "Güvenlik anahtarını kaldırmak için lütfen iki faktörlü kimlik doğrulama kodunuzu girin", - "securityKeyTwoFactorCode": "İki Faktörlü Kod", - "securityKeyRemoveTitle": "Güvenlik Anahtarını Kaldır", - "securityKeyRemoveDescription": "Güvenlik anahtarını \"{name}\" kaldırmak için şifrenizi girin", - "securityKeyNoKeysRegistered": "Kayıtlı güvenlik anahtarı yok", - "securityKeyNoKeysDescription": "Hesabınızın güvenliğini artırmak için bir güvenlik anahtarı ekleyin", - "createDomainRequired": "Alan adı gereklidir", - "createDomainAddDnsRecords": "DNS Kayıtlarını Ekle", - "createDomainAddDnsRecordsDescription": "Kurulumu tamamlamak için alan sağlayıcınıza şu DNS kayıtlarını ekleyin.", - "createDomainNsRecords": "NS Kayıtları", - "createDomainRecord": "Kayıt", - "createDomainType": "Tür:", - "createDomainName": "Ad:", - "createDomainValue": "Değer:", - "createDomainCnameRecords": "CNAME Kayıtları", - "createDomainARecords": "A Kayıtları", - "createDomainRecordNumber": "Kayıt {number}", - "createDomainTxtRecords": "TXT Kayıtları", - "createDomainSaveTheseRecords": "Bu Kayıtları Kaydet", - "createDomainSaveTheseRecordsDescription": "Bu DNS kayıtlarını kaydettiğinizden emin olun çünkü tekrar görmeyeceksiniz.", - "createDomainDnsPropagation": "DNS Yayılması", - "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ı", - "and": "ve", - "privacyPolicy": "gizlilik politikası" - }, - "siteRequired": "Site gerekli.", - "olmTunnel": "Olm Tüneli", - "olmTunnelDescription": "Müşteri bağlantıları için Olm kullanın", - "errorCreatingClient": "Müşteri oluşturulurken hata oluştu", - "clientDefaultsNotFound": "Müşteri varsayılanları bulunamadı", - "createClient": "Müşteri Oluştur", - "createClientDescription": "Sitelerinize bağlanmak için yeni bir müşteri oluşturun", - "seeAllClients": "Tüm Müşterileri Gör", - "clientInformation": "Müşteri Bilgileri", - "clientNamePlaceholder": "Müşteri adı", - "address": "Adres", - "subnetPlaceholder": "Alt ağ", - "addressDescription": "Bu müşteri için bağlantıda kullanılacak adres", - "selectSites": "Siteleri seçin", - "sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır", - "clientInstallOlm": "Olm Yükle", - "clientInstallOlmDescription": "Sisteminizde Olm çalıştırın", - "clientOlmCredentials": "Olm Kimlik Bilgileri", - "clientOlmCredentialsDescription": "Bu, Olm'in sunucu ile kimlik doğrulaması yapacağı yöntemdir", - "olmEndpoint": "Olm Uç Noktası", - "olmId": "Olm Kimliği", - "olmSecretKey": "Olm Gizli Anahtarı", - "clientCredentialsSave": "Kimlik Bilgilerinizi Kaydedin", - "clientCredentialsSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.", - "generalSettingsDescription": "Bu müşteri için genel ayarları yapılandırın", - "clientUpdated": "Müşteri güncellendi", - "clientUpdatedDescription": "Müşteri güncellenmiştir.", - "clientUpdateFailed": "Müşteri güncellenemedi", - "clientUpdateError": "Müşteri güncellenirken bir hata oluştu.", - "sitesFetchFailed": "Siteler alınamadı", - "sitesFetchError": "Siteler alınırken bir hata oluştu.", - "olmErrorFetchReleases": "Olm yayınları alınırken bir hata oluştu.", - "olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.", - "remoteSubnets": "Uzak Alt Ağlar", - "enterCidrRange": "CIDR aralığını girin", - "remoteSubnetsDescription": "Bu siteye uzaktan erişilebilen CIDR aralıklarını ekleyin. 10.0.0.0/24 formatını kullanın. Bu YALNIZCA VPN istemci bağlantıları için geçerlidir.", - "resourceEnableProxy": "Genel Proxy'i Etkinleştir", - "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", - "externalProxyEnabled": "Dış Proxy Etkinleştirildi", - "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ı", - "domainPickerBaseDomainLabel": "Temel Alan Adı", - "domainPickerSearchDomains": "Alan adlarını ara...", - "domainPickerNoDomainsFound": "Hiçbir alan adı bulunamadı", - "domainPickerLoadingDomains": "Alan adları yükleniyor...", - "domainPickerSelectBaseDomain": "Temel alan adını seçin...", - "domainPickerNotAvailableForCname": "CNAME alan adları için kullanılabilir değil", - "domainPickerEnterSubdomainOrLeaveBlank": "Alt alan adını girin veya temel alan adını kullanmak için boş bırakın.", - "domainPickerEnterSubdomainToSearch": "Mevcut ücretsiz alan adları arasından aramak ve seçmek için bir alt alan adı girin.", - "domainPickerFreeDomains": "Ücretsiz Alan Adları", - "domainPickerSearchForAvailableDomains": "Mevcut alan adlarını ara", - "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ı", - "proxyPort": "Bağlantı Noktası", - "resourcesTableProxyResources": "Proxy Kaynaklar", - "resourcesTableClientResources": "İstemci Kaynaklar", - "resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.", - "resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.", - "resourcesTableDestination": "Hedef", - "resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için", - "resourcesTableClients": "İstemciler", - "resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.", - "editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin", - "editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.", - "editInternalResourceDialogResourceProperties": "Kaynak Özellikleri", - "editInternalResourceDialogName": "Ad", - "editInternalResourceDialogProtocol": "Protokol", - "editInternalResourceDialogSitePort": "Site Bağlantı Noktası", - "editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", - "editInternalResourceDialogCancel": "İptal", - "editInternalResourceDialogSaveResource": "Kaynağı Kaydet", - "editInternalResourceDialogSuccess": "Başarı", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Dahili kaynak başarıyla güncellendi", - "editInternalResourceDialogError": "Hata", - "editInternalResourceDialogFailedToUpdateInternalResource": "Dahili kaynak güncellenemedi", - "editInternalResourceDialogNameRequired": "Ad gerekli", - "editInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır", - "editInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır", - "editInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır", - "editInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı", - "editInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır", - "editInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır", - "createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı", - "createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.", - "createInternalResourceDialogClose": "Kapat", - "createInternalResourceDialogCreateClientResource": "İstemci Kaynağı Oluştur", - "createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemciler için erişilebilir olacak yeni bir kaynak oluşturun.", - "createInternalResourceDialogResourceProperties": "Kaynak Özellikleri", - "createInternalResourceDialogName": "Ad", - "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Site seç...", - "createInternalResourceDialogSearchSites": "Siteleri ara...", - "createInternalResourceDialogNoSitesFound": "Site bulunamadı.", - "createInternalResourceDialogProtocol": "Protokol", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "Site Bağlantı Noktası", - "createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.", - "createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma", - "createInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.", - "createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.", - "createInternalResourceDialogCancel": "İptal", - "createInternalResourceDialogCreateResource": "Kaynak Oluştur", - "createInternalResourceDialogSuccess": "Başarı", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Dahili kaynak başarıyla oluşturuldu", - "createInternalResourceDialogError": "Hata", - "createInternalResourceDialogFailedToCreateInternalResource": "Dahili kaynak oluşturulamadı", - "createInternalResourceDialogNameRequired": "Ad gerekli", - "createInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır", - "createInternalResourceDialogPleaseSelectSite": "Lütfen bir site seçin", - "createInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır", - "createInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır", - "createInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı", - "createInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır", - "createInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır", - "siteConfiguration": "Yapılandırma", - "siteAcceptClientConnections": "İstemci Bağlantılarını Kabul Et", - "siteAcceptClientConnectionsDescription": "Bu Newt örneğini bir geçit olarak kullanarak diğer cihazların bağlanmasına izin verin.", - "siteAddress": "Site Adresi", - "siteAddressDescription": "İstemcilerin bağlanması için hostun IP adresini belirtin. Bu, Pangolin ağındaki sitenin iç adresidir ve istemciler için atlas olmalıdır. Org alt ağına düşmelidir.", - "autoLoginExternalIdp": "Harici IDP ile Otomatik Giriş", - "autoLoginExternalIdpDescription": "Kullanıcıyı kimlik doğrulama için otomatik olarak harici IDP'ye yönlendirin.", - "selectIdp": "IDP Seç", - "selectIdpPlaceholder": "IDP seçin...", - "selectIdpRequired": "Otomatik giriş etkinleştirildiğinde lütfen bir IDP seçin.", - "autoLoginTitle": "Yönlendiriliyor", - "autoLoginDescription": "Kimlik doğrulama için harici kimlik sağlayıcıya yönlendiriliyorsunuz.", - "autoLoginProcessing": "Kimlik doğrulama hazırlanıyor...", - "autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...", - "autoLoginError": "Otomatik Giriş Hatası", - "autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.", - "autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.", - "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", - "introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin", - "introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.", - "introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz — tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:", - "benefitSimplerOperations": { - "title": "Daha basit işlemler", - "description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız." - }, - "benefitAutomaticUpdates": { - "title": "Otomatik güncellemeler", - "description": "Bulut panosu hızla gelişir, böylece her seferinde yeni konteynerler manuel olarak çekmeden yeni özellikler ve hata düzeltmeleri alırsınız." - }, - "benefitLessMaintenance": { - "title": "Daha az bakım", - "description": "Veritabanı geçişleri, yedeklemeler veya ekstra altyapı yönetimi yok. Biz bunu bulutta hallederiz." - }, - "benefitCloudFailover": { - "title": "Bulut yedekleme", - "description": "Düğümünüz kapandığında, tünelleriniz geçici olarak bulut bağlantı noktalarımıza geçebilir, böylece tekrar çevrimiçi hale getirene kadar tünelleriniz kesintiye uğramaz." - }, - "benefitHighAvailability": { - "title": "Yüksek kullanılabilirlik (Bağlantı Noktaları)", - "description": "Yedeklilik ve daha iyi performans için hesabınıza birden fazla düğüm bağlayabilirsiniz." - }, - "benefitFutureEnhancements": { - "title": "Gelecek iyileştirmeler", - "description": "Dağıtımınızı daha sağlam hale getirmek amacıyla daha fazla analiz, uyarı ve yönetim aracı eklemeyi planlıyoruz." - }, - "docsAlert": { - "text": "Yönetilen Kendi Kendine Barındırılan seçeneği hakkında daha fazla bilgi edinin", - "documentation": "dokümantasyon" - }, - "convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün" - }, - "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ı", - "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ı", - "domainPickerUnverified": "Doğrulanmadı", - "domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.", - "domainPickerError": "Hata", - "domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi", - "domainPickerErrorCheckAvailability": "Alan adı kullanılabilirliği kontrol edilemedi", - "domainPickerInvalidSubdomain": "Geçersiz alt alan adı", - "domainPickerInvalidSubdomainRemoved": "Girdi \"{sub}\" geçersiz olduğu için kaldırıldı.", - "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", - "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 deleted file mode 100644 index 508f3f18..00000000 --- a/messages/zh-CN.json +++ /dev/null @@ -1,1897 +0,0 @@ -{ - "setupCreate": "创建您的第一个组织、网站和资源", - "setupNewOrg": "新建组织", - "setupCreateOrg": "创建组织", - "setupCreateResources": "创建资源", - "setupOrgName": "组织名称", - "orgDisplayName": "这是您组织的显示名称。", - "orgId": "组织ID", - "setupIdentifierMessage": "这是您组织的唯一标识符。这是与显示名称分开的。", - "setupErrorIdentifier": "组织ID 已被使用。请另选一个。", - "componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。", - "componentsErrorNoMember": "您目前不是任何组织的成员。", - "welcome": "欢迎使用 Pangolin", - "welcomeTo": "欢迎来到", - "componentsCreateOrg": "创建组织", - "componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。", - "componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。", - "dismiss": "忽略", - "componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。", - "componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。", - "inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。", - "inviteErrorUser": "很抱歉,但看起来你想要访问的邀请不是这个用户。", - "inviteLoginUser": "请确保您以正确的用户登录。", - "inviteErrorNoUser": "很抱歉,但看起来你想访问的邀请不是一个存在的用户。", - "inviteCreateUser": "请先创建一个帐户。", - "goHome": "返回首页", - "inviteLogInOtherUser": "以不同的用户登录", - "createAnAccount": "创建帐户", - "inviteNotAccepted": "邀请未接受", - "authCreateAccount": "创建一个帐户以开始", - "authNoAccount": "没有账户?", - "email": "电子邮件地址", - "password": "密码", - "confirmPassword": "确认密码", - "createAccount": "创建帐户", - "viewSettings": "查看设置", - "delete": "删除", - "name": "名称", - "online": "在线", - "offline": "离线的", - "site": "站点", - "dataIn": "数据输入", - "dataOut": "数据输出", - "connectionType": "连接类型", - "tunnelType": "隧道类型", - "local": "本地的", - "edit": "编辑", - "siteConfirmDelete": "确认删除站点", - "siteDelete": "删除站点", - "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 配置", - "dockerRun": "停靠栏", - "siteLearnLocal": "本地站点不需要隧道连接,点击了解更多", - "siteConfirmCopy": "我已经复制了配置信息", - "searchSitesProgress": "搜索站点...", - "siteAdd": "添加站点", - "siteInstallNewt": "安装 Newt", - "siteInstallNewtDescription": "在您的系统中运行 Newt", - "WgConfiguration": "WireGuard 配置", - "WgConfigurationDescription": "使用以下配置连接到您的网络", - "operatingSystem": "操作系统", - "commands": "命令", - "recommended": "推荐", - "siteNewtDescription": "为获得最佳用户体验,请使用 Newt。其底层采用 WireGuard 技术,可直接通过 Pangolin 控制台,使用局域网地址访问您私有网络中的资源。", - "siteRunsInDocker": "在 Docker 中运行", - "siteRunsInShell": "在 macOS 、 Linux 和 Windows 的 Shell 中运行", - "siteErrorDelete": "删除站点出错", - "siteErrorUpdate": "更新站点失败", - "siteErrorUpdateDescription": "更新站点时出错。", - "siteUpdated": "站点已更新", - "siteUpdatedDescription": "网站已更新。", - "siteGeneralDescription": "配置此站点的常规设置", - "siteSettingDescription": "配置您网站上的设置", - "siteSetting": "{siteName} 设置", - "siteNewtTunnel": "Newt 隧道 (推荐)", - "siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。", - "siteWg": "基本 WireGuard", - "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", - "siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。", - "siteLocalDescription": "仅限本地资源。不需要隧道。", - "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": "您的访问令牌可以通过两种方式传递:作为查询参数或请求头。 每次验证访问请求都必须从客户端传递。", - "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": "缺少组织 ID", - "orgMissingMessage": "没有组织ID,无法重新生成邀请。", - "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, 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": "您确定要从组织中删除 \"{selectedApiKey}\" API密钥吗?", - "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 Commercial License和订阅条款", - "licenseMessageRemove": "这将删除许可证密钥和它授予的所有相关权限。", - "licenseMessageConfirm": "要确认,请在下面输入许可证密钥。", - "licenseQuestionRemove": "您确定要删除 {selectedKey} 的邀请吗?", - "licenseKeyDelete": "删除许可证密钥", - "licenseKeyDeleteConfirm": "确认删除许可证密钥", - "licenseTitle": "管理许可证状态", - "licenseTitleDescription": "查看和管理系统中的许可证密钥", - "licenseHost": "主机许可证", - "licenseHostDescription": "管理主机的主许可证密钥。", - "licensedNot": "未授权", - "hostId": "主机 ID", - "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": "为 {email} 创建的邀请已删除", - "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, 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": "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": "SNI使用的 TLS 服务器名称。留空使用默认值。", - "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": "在 CIDR 符号中匹配一系列IP地址", - "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 私钥", - "architecture": "架构", - "sites": "站点", - "siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决您的内部资源。", - "siteWgCompatibleAllClients": "与所有WireGuard客户端兼容", - "siteWgManualConfigurationRequired": "需要手动配置", - "userErrorNotAdminOrOwner": "用户不是管理员或所有者", - "pangolinSettings": "设置 - Pangolin", - "accessRoleYour": "您的角色:", - "accessRoleSelect2": "选择角色", - "accessUserSelect": "选择一个用户", - "otpEmailEnter": "输入电子邮件", - "otpEmailEnterDescription": "在输入字段输入后按回车键添加电子邮件。", - "otpEmailErrorInvalid": "无效的邮箱地址。通配符(*)必须占据整个开头部分。", - "otpEmailSmtpRequired": "需要先配置SMTP", - "otpEmailSmtpRequiredDescription": "必须在服务器上启用SMTP才能使用一次性密码验证。", - "otpEmailTitle": "一次性密码", - "otpEmailTitleDescription": "资源访问需要基于电子邮件的身份验证", - "otpEmailWhitelist": "电子邮件白名单", - "otpEmailWhitelistList": "白名单邮件", - "otpEmailWhitelistListDescription": "只有拥有这些电子邮件地址的用户才能访问此资源。 他们将被提示输入一次性密码发送到他们的电子邮件。 通配符 (*@example.com) 可以用来允许来自一个域名的任何电子邮件地址。", - "otpEmailWhitelistSave": "保存白名单", - "passwordAdd": "添加密码", - "passwordRemove": "删除密码", - "pincodeAdd": "添加 PIN 码", - "pincodeRemove": "移除 PIN 码", - "resourceAuthMethods": "身份验证方法", - "resourceAuthMethodsDescriptions": "允许通过额外的认证方法访问资源", - "resourceAuthSettingsSave": "保存成功", - "resourceAuthSettingsSaveDescription": "已保存身份验证设置", - "resourceErrorAuthFetch": "获取数据失败", - "resourceErrorAuthFetchDescription": "获取数据时出错", - "resourceErrorPasswordRemove": "删除资源密码出错", - "resourceErrorPasswordRemoveDescription": "删除资源密码时出错", - "resourceErrorPasswordSetup": "设置资源密码出错", - "resourceErrorPasswordSetupDescription": "设置资源密码时出错", - "resourceErrorPincodeRemove": "删除资源固定码时出错", - "resourceErrorPincodeRemoveDescription": "删除资源PIN码时出错", - "resourceErrorPincodeSetup": "设置资源 PIN 码时出错", - "resourceErrorPincodeSetupDescription": "设置资源 PIN 码时发生错误", - "resourceErrorUsersRolesSave": "设置角色失败", - "resourceErrorUsersRolesSaveDescription": "设置角色时出错", - "resourceErrorWhitelistSave": "保存白名单失败", - "resourceErrorWhitelistSaveDescription": "保存白名单时出错", - "resourcePasswordSubmit": "启用密码保护", - "resourcePasswordProtection": "密码保护 {status}", - "resourcePasswordRemove": "已删除资源密码", - "resourcePasswordRemoveDescription": "已成功删除资源密码", - "resourcePasswordSetup": "设置资源密码", - "resourcePasswordSetupDescription": "已成功设置资源密码", - "resourcePasswordSetupTitle": "设置密码", - "resourcePasswordSetupTitleDescription": "设置密码来保护此资源", - "resourcePincode": "PIN 码", - "resourcePincodeSubmit": "启用 PIN 码保护", - "resourcePincodeProtection": "PIN 码保护 {status}", - "resourcePincodeRemove": "资源 PIN 码已删除", - "resourcePincodeRemoveDescription": "已成功删除资源 PIN 码", - "resourcePincodeSetup": "资源 PIN 码已设置", - "resourcePincodeSetupDescription": "资源 PIN 码已成功设置", - "resourcePincodeSetupTitle": "设置 PIN 码", - "resourcePincodeSetupTitleDescription": "设置 PIN 码来保护此资源", - "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": "服务器管理员 - Pangolin", - "licenseTierProfessional": "专业许可证", - "licenseTierEnterprise": "企业许可证", - "licenseTierPersonal": "Personal License", - "licensed": "已授权", - "yes": "是", - "no": "否", - "sitesAdditional": "其他站点", - "licenseKeys": "许可证密钥", - "sitestCountDecrease": "减少站点数量", - "sitestCountIncrease": "增加站点数量", - "idpManage": "管理身份提供商", - "idpManageDescription": "查看和管理系统中的身份提供商", - "idpDeletedDescription": "身份提供商删除成功", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "你确定要永久删除 \"{name}\" 这个身份提供商吗?", - "idpMessageRemove": "这将删除身份提供者和所有相关的配置。通过此提供者进行身份验证的用户将无法登录。", - "idpMessageConfirm": "要确认,请在下面输入身份提供者的名称。", - "idpConfirmDelete": "确认删除身份提供商", - "idpDelete": "删除身份提供商", - "idp": "身份提供商", - "idpSearch": "搜索身份提供者...", - "idpAdd": "添加身份提供商", - "idpClientIdRequired": "客户端ID 是必需的。", - "idpClientSecretRequired": "客户端密钥是必需的。", - "idpErrorAuthUrlInvalid": "身份验证URL 必须是有效的 URL。", - "idpErrorTokenUrlInvalid": "令牌URL 必须是有效的 URL。", - "idpPathRequired": "标识符路径是必需的。", - "idpScopeRequired": "授权范围是必需的。", - "idpOidcDescription": "配置 OpenID 连接身份提供商", - "idpCreatedDescription": "身份提供商创建成功", - "idpCreate": "创建身份提供商", - "idpCreateDescription": "配置用户身份验证的新身份提供商", - "idpSeeAll": "查看所有身份提供商", - "idpSettingsDescription": "配置身份提供者的基本信息", - "idpDisplayName": "此身份提供商的显示名称", - "idpAutoProvisionUsers": "自动提供用户", - "idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。", - "licenseBadge": "EE", - "idpType": "提供者类型", - "idpTypeDescription": "选择您想要配置的身份提供者类型", - "idpOidcConfigure": "OAuth2/OIDC 配置", - "idpOidcConfigureDescription": "配置 OAuth2/OIDC 供应商端点和凭据", - "idpClientId": "客户端ID", - "idpClientIdDescription": "来自您身份提供商的 OAuth2 客户端 ID", - "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": "作用域(Scopes)", - "idpOidcConfigureScopesDescription": "以空格分隔的 OAuth2 请求作用域列表", - "idpSubmit": "创建身份提供商", - "orgPolicies": "组织策略", - "idpSettings": "{idpName} 设置", - "idpCreateSettingsDescription": "配置身份提供商的设置", - "roleMapping": "角色映射", - "orgMapping": "组织映射", - "orgPoliciesSearch": "搜索组织策略...", - "orgPoliciesAdd": "添加组织策略", - "orgRequired": "组织是必填项", - "error": "错误", - "success": "成功", - "orgPolicyAddedDescription": "策略添加成功", - "orgPolicyUpdatedDescription": "策略更新成功", - "orgPolicyDeletedDescription": "已成功删除策略", - "defaultMappingsUpdatedDescription": "默认映射更新成功", - "orgPoliciesAbout": "关于组织政策", - "orgPoliciesAboutDescription": "组织策略用于根据用户的 ID 令牌来控制对组织的访问。 您可以指定 JMESPath 表达式来提取角色和组织信息从 ID 令牌中提取信息。", - "orgPoliciesAboutDescriptionLink": "欲了解更多信息,请参阅文件。", - "defaultMappingsOptional": "默认映射(可选)", - "defaultMappingsOptionalDescription": "当没有为某个组织定义组织的政策时,使用默认映射。 您可以指定默认角色和组织映射回到这里。", - "defaultMappingsRole": "默认角色映射", - "defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。", - "defaultMappingsOrg": "默认组织映射", - "defaultMappingsOrgDescription": "此表达式必须返回 组织ID 或 true 才能允许用户访问组织。", - "defaultMappingsSubmit": "保存默认映射", - "orgPoliciesEdit": "编辑组织策略", - "org": "组织", - "orgSelect": "选择组织", - "orgSearch": "搜索", - "orgNotFound": "找不到组织。", - "roleMappingPathOptional": "角色映射路径(可选)", - "orgMappingPathOptional": "组织映射路径(可选)", - "orgPolicyUpdate": "更新策略", - "orgPolicyAdd": "添加策略", - "orgPolicyConfig": "配置组织访问权限", - "idpUpdatedDescription": "身份提供商更新成功", - "redirectUrl": "重定向网址", - "redirectUrlAbout": "关于重定向网址", - "redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供商设置中配置此URL。", - "pangolinAuth": "认证 - 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": "PIN码必须是6位数字", - "pincodeRequirementsChars": "PIN 必须只包含数字", - "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": "Pincode 验证失败", - "passwordErrorAuthenticate": "密码验证失败", - "poweredBy": "支持者:", - "authenticationRequired": "需要身份验证", - "authenticationMethodChoose": "请选择您偏好的方式来访问 {name}", - "authenticationRequest": "您必须通过身份验证才能访问 {name}", - "user": "用户", - "pincodeInput": "6位数字 PIN 码", - "pincodeSubmit": "使用PIN登录", - "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": "找不到 IdP", - "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": "已输入的标签", - "tagsEnteredDescription": "这些是您输入的标签。", - "tagsWarnCannotBeLessThanZero": "最大标签和最小标签不能小于 0", - "tagsWarnNotAllowedAutocompleteOptions": "标记不允许为每个自动完成选项", - "tagsWarnInvalid": "无效的标签,每个有效标签", - "tagWarnTooShort": "标签 {tagText} 太短", - "tagWarnTooLong": "标签 {tagText} 太长", - "tagsWarnReachedMaxNumber": "已达到允许标签的最大数量", - "tagWarnDuplicate": "未添加重复标签 {tagText}", - "supportKeyInvalid": "无效密钥", - "supportKeyInvalidDescription": "您的支持者密钥无效。", - "supportKeyValid": "有效的密钥", - "supportKeyValidDescription": "您的支持者密钥已被验证。感谢您的支持!", - "supportKeyErrorValidationDescription": "验证支持者密钥失败。", - "supportKey": "支持开发和通过一个 Pangolin !", - "supportKeyDescription": "购买支持者钥匙,帮助我们继续为社区发展 Pangolin 。 您的贡献使我们能够投入更多的时间来维护和添加所有人的新功能。 我们永远不会用这个来支付墙上的功能。这与任何商业版是分开的。", - "supportKeyPet": "您还可以领养并见到属于自己的 Pangolin!", - "supportKeyPurchase": "付款通过 GitHub 进行处理,之后您可以在以下位置获取您的密钥:", - "supportKeyPurchaseLink": "我们的网站", - "supportKeyPurchase2": "并在这里兑换。", - "supportKeyLearnMore": "了解更多。", - "supportKeyOptions": "请选择最适合您的选项。", - "supportKetOptionFull": "完全支持者", - "forWholeServer": "适用于整个服务器", - "lifetimePurchase": "终身购买", - "supporterStatus": "支持者状态", - "buy": "购买", - "supportKeyOptionLimited": "有限支持者", - "forFiveUsers": "适用于 5 或更少用户", - "supportKeyRedeem": "兑换支持者密钥", - "supportKeyHideSevenDays": "隐藏7天", - "supportKeyEnter": "输入支持者密钥", - "supportKeyEnterDescription": "见到你自己的 Pangolin!", - "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": "用您的身份验证程序扫描此二维码或手动输入密钥:", - "otpSetupSecretCode": "验证器代码", - "otpSetupSuccess": "启用两步验证", - "otpSetupSuccessStoreBackupCodes": "您的帐户现在更加安全。不要忘记保存您的备份代码。", - "otpErrorDisable": "无法禁用 2FA", - "otpErrorDisableDescription": "禁用2FA 时出错", - "otpRemove": "禁用两步验证", - "otpRemoveDescription": "为您的帐户禁用两步验证", - "otpRemoveSuccess": "双重身份验证已禁用", - "otpRemoveSuccessMessage": "您的帐户已禁用双重身份验证。您可以随时再次启用它。", - "otpRemoveSubmit": "禁用两步验证", - "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 标签擦除蓝图标签。套接字路径必须提供给新的。", - "enableDockerSocketLink": "了解更多", - "viewDockerContainers": "查看停靠容器", - "containersIn": "{siteName} 中的容器", - "selectContainerDescription": "选择任何容器作为目标的主机名。点击端口使用端口。", - "containerName": "名称", - "containerImage": "图片", - "containerState": "状态", - "containerNetworks": "网络", - "containerHostnameIp": "主机名/IP", - "containerLabels": "标签", - "containerLabelsCount": "{count, plural, other {# 标签}}", - "containerLabelsTitle": "容器标签", - "containerLabelEmpty": "<为空>", - "containerPorts": "端口", - "containerPortsMore": "+{count} 更多", - "containerActions": "行动", - "select": "选择", - "noContainersMatchingFilters": "没有找到匹配当前过滤器的容器。", - "showContainersWithoutPorts": "显示没有端口的容器", - "showStoppedContainers": "显示已停止的容器", - "noContainersFound": "未找到容器。请确保Docker容器正在运行。", - "searchContainersPlaceholder": "在 {count} 个容器中搜索...", - "searchResultsCount": "{count, plural, 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 托管", - "fossorial": "Fossorial", - "completeAccountSetup": "完成账户设置", - "completeAccountSetupDescription": "设置您的密码以开始", - "accountSetupSent": "我们将发送账号设置代码到该电子邮件地址。", - "accountSetupCode": "设置代码", - "accountSetupCodeDescription": "请检查您的邮箱以获取设置代码。", - "passwordCreate": "创建密码", - "passwordCreateConfirm": "确认密码", - "accountSetupSubmit": "发送设置代码", - "completeSetup": "完成设置", - "accountSetupSuccess": "账号设置完成!欢迎来到 Pangolin!", - "documentation": "文档", - "saveAllSettings": "保存所有设置", - "settingsUpdated": "设置已更新", - "settingsUpdatedDescription": "所有设置已成功更新", - "settingsErrorUpdate": "设置更新失败", - "settingsErrorUpdateDescription": "更新设置时发生错误", - "sidebarCollapse": "折叠", - "sidebarExpand": "展开", - "newtUpdateAvailable": "更新可用", - "newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。", - "domainPickerEnterDomain": "域名", - "domainPickerPlaceholder": "example.com", - "domainPickerDescription": "输入资源的完整域名以查看可用选项。", - "domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。", - "domainPickerTabAll": "所有", - "domainPickerTabOrganization": "组织", - "domainPickerTabProvided": "提供的", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "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": "无法获取结账网址", - "billingPleaseTryAgainLater": "请稍后再试。", - "billingCheckoutError": "结账错误", - "billingFailedToGetPortalUrl": "无法获取门户网址", - "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": "我同意", - "termsOfService": "服务条款", - "and": "和", - "privacyPolicy": "隐私政策" - }, - "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 私钥", - "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": "头部新行分隔:头部名称:值", - "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": "站点端口", - "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地址以供客户端连接。这是Pangolin网络中站点的内部地址,供客户端访问。必须在Org子网内。", - "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": "确认删除节点", - "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 服务器,带有额外的铃声和告密器", - "introTitle": "托管自托管的潘戈林公司", - "introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。", - "introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 — — 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:", - "benefitSimplerOperations": { - "title": "简单的操作", - "description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。" - }, - "benefitAutomaticUpdates": { - "title": "自动更新", - "description": "云仪表盘快速演化,所以您可以获得新的功能和错误修复,而不必每次手动拉取新的容器。" - }, - "benefitLessMaintenance": { - "title": "减少维护时间", - "description": "没有要管理的数据库迁移、备份或额外的基础设施。我们在云端处理这个问题。" - }, - "benefitCloudFailover": { - "title": "云失败", - "description": "如果您的节点被关闭,您的隧道可能暂时无法连接到我们的云端,直到您将其重新连接上线。" - }, - "benefitHighAvailability": { - "title": "高可用率(PoPs)", - "description": "您还可以将多个节点添加到您的帐户中以获取冗余和更好的性能。" - }, - "benefitFutureEnhancements": { - "title": "将来的改进", - "description": "我们正在计划添加更多的分析、警报和管理工具,使你的部署更加有力。" - }, - "docsAlert": { - "text": "在我们中更多地了解管理下的自托管选项", - "documentation": "文档" - }, - "convertButton": "将此节点转换为管理自托管的" - }, - "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", - "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": "需要电子邮件验证。 请通过 {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/next.config.mjs b/next.config.mjs index c870f1c1..fce5b1fa 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,13 +1,9 @@ -import createNextIntlPlugin from "next-intl/plugin"; - -const withNextIntl = createNextIntlPlugin(); - -/** @type {import("next").NextConfig} */ +/** @type {import('next').NextConfig} */ const nextConfig = { eslint: { - ignoreDuringBuilds: true + ignoreDuringBuilds: true, }, output: "standalone" }; -export default withNextIntl(nextConfig); +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 460cc451..20cb9e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,127 +9,115 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "5.2.2", - "@node-rs/argon2": "^2.0.2", + "@asteasolutions/zod-to-openapi": "^7.3.0", + "@hookform/resolvers": "3.9.1", + "@node-rs/argon2": "2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", "@radix-ui/react-icons": "1.3.2", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.6", - "@react-email/render": "^1.3.2", - "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.2.2", - "@simplewebauthn/server": "^13.2.2", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "^1.1.4", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@react-email/components": "0.0.36", + "@react-email/render": "^1.0.6", + "@react-email/tailwind": "1.0.4", "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-table": "8.21.3", - "arctic": "^3.7.0", - "axios": "^1.12.2", + "@tanstack/react-table": "8.20.6", + "arctic": "^3.6.0", + "axios": "1.8.4", "better-sqlite3": "11.7.0", "canvas-confetti": "1.9.3", - "class-variance-authority": "^0.7.1", + "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "cmdk": "1.1.1", + "cmdk": "1.0.4", "cookie": "^1.0.2", "cookie-parser": "1.4.7", "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "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", - "helmet": "8.1.0", + "drizzle-orm": "0.38.3", + "eslint": "9.17.0", + "eslint-config-next": "15.1.3", + "express": "4.21.2", + "express-rate-limit": "7.5.0", + "glob": "11.0.0", + "helmet": "8.0.0", "http-errors": "2.0.0", "i": "^0.3.7", - "input-otp": "1.4.2", + "input-otp": "1.4.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.545.0", - "maxmind": "5.0.0", + "lucide-react": "0.469.0", "moment": "2.30.1", - "next": "15.5.4", - "next-intl": "^4.3.12", - "next-themes": "0.4.6", + "next": "15.2.4", + "next-themes": "0.4.4", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.9", - "npm": "^11.6.2", + "nodemailer": "6.9.16", + "npm": "^11.2.0", "oslo": "1.2.1", - "pg": "^8.16.2", "qrcode.react": "4.2.0", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-easy-sort": "^1.8.0", - "react-hook-form": "7.65.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-easy-sort": "^1.6.0", + "react-hook-form": "7.54.2", "react-icons": "^5.5.0", "rebuild": "0.1.2", - "resend": "^6.1.2", - "semver": "^7.7.3", + "semver": "7.6.3", "swagger-ui-express": "^5.0.1", - "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.8", - "uuid": "^13.0.0", + "tailwind-merge": "2.6.0", + "tw-animate-css": "^1.2.5", + "uuid": "^11.1.0", "vaul": "1.1.2", - "winston": "3.18.3", + "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.3", - "yargs": "18.0.0", - "zod": "3.25.76", - "zod-validation-error": "3.5.2" + "ws": "8.18.0", + "zod": "3.24.1", + "zod-validation-error": "3.4.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.0", + "@dotenvx/dotenvx": "1.32.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "4.3.0", - "@tailwindcss/postcss": "^4.1.14", + "@tailwindcss/postcss": "^4.1.3", "@types/better-sqlite3": "7.6.12", - "@types/cookie-parser": "1.4.9", - "@types/cors": "2.8.19", + "@types/cookie-parser": "1.4.8", + "@types/cors": "2.8.17", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.3", - "@types/express-session": "^1.18.2", + "@types/express": "5.0.0", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.7.2", - "@types/nodemailer": "7.0.2", - "@types/pg": "8.15.5", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.1", - "@types/semver": "^7.7.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^22", + "@types/nodemailer": "6.4.17", + "@types/react": "19.1.1", + "@types/react-dom": "19.1.2", + "@types/semver": "7.5.8", "@types/swagger-ui-express": "^4.1.8", - "@types/ws": "8.18.1", + "@types/ws": "8.5.13", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.5", - "esbuild": "0.25.10", + "drizzle-kit": "0.30.6", + "esbuild": "0.25.2", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.3.0", + "react-email": "4.0.6", "tailwindcss": "^4.1.4", - "tsc-alias": "1.8.16", - "tsx": "4.20.6", + "tsc-alias": "1.8.10", + "tsx": "4.19.3", "typescript": "^5", - "typescript-eslint": "^8.46.0" + "yargs": "17.7.2" } }, "node_modules/@alloc/quick-lru": { @@ -145,24 +133,10 @@ "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", - "integrity": "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.0.tgz", + "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" @@ -171,822 +145,32 @@ "zod": "^3.20.2" } }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2": { - "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.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-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.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/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.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/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.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/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.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/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.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/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.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/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.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/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.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/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.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/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.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/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.910.0", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "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.910.0", - "@aws/lambda-invoke-store": "^0.0.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/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.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/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.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/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.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/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.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/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.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/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.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/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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "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/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.910.0", - "@smithy/types": "^4.7.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "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.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": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", - "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "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": { - "@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==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "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", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" }, "engines": { @@ -994,205 +178,6 @@ } }, "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", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "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", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "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/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==", @@ -1208,29 +193,62 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "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==", + "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.28.4" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1240,17 +258,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "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", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1258,15 +276,41 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "node_modules/@babel/traverse/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/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "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": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1282,25 +326,25 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "license": "MIT", "dependencies": { - "@so-ric/colorspace": "^1.1.6", + "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", - "integrity": "sha512-CbMGzyOYSyFF7d4uaeYwO9gpSBzLTnMmSmTVpCZjvpJFV69qYbjYPpzNnCz1mb2wIvEhjWjRwQWuBzTO0jITww==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.32.0.tgz", + "integrity": "sha512-oQaGYijYfQx6pY9D+FQ08gUOckF1R0RSVK7Jqk+Ma2RyeceoMIawQl1KoogRaJ12i0SmyVWhiGyQxDU01/k13g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", - "dotenv": "^17.2.1", + "dotenv": "^16.4.5", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", @@ -1310,7 +354,8 @@ "which": "^4.0.0" }, "bin": { - "dotenvx": "src/cli/dotenvx.js" + "dotenvx": "src/cli/dotenvx.js", + "git-dotenvx": "src/cli/dotenvx.js" }, "funding": { "url": "https://dotenvx.com" @@ -1324,9 +369,9 @@ "license": "Apache-2.0" }, "node_modules/@ecies/ciphers": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.4.tgz", - "integrity": "sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz", + "integrity": "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==", "dev": true, "license": "MIT", "engines": { @@ -1339,20 +384,19 @@ } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", "license": "MIT", "optional": true, "dependencies": { @@ -1360,9 +404,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "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==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", "license": "MIT", "optional": true, "dependencies": { @@ -1822,9 +866,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -1839,9 +883,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -1856,9 +900,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -1873,9 +917,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -1890,9 +934,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -1907,9 +951,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -1924,9 +968,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -1941,9 +985,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -1958,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -1975,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -1992,9 +1036,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -2009,9 +1053,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -2026,9 +1070,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -2043,9 +1087,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -2060,9 +1104,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -2077,9 +1121,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -2094,9 +1138,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -2111,9 +1155,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -2128,9 +1172,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -2145,9 +1189,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -2162,9 +1206,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -2178,27 +1222,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -2213,9 +1240,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -2230,9 +1257,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -2247,9 +1274,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -2264,9 +1291,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2303,12 +1330,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -2316,22 +1343,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-helpers": { - "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.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2341,9 +1356,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2363,78 +1378,75 @@ "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.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "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==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/react-dom": { - "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==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -2442,87 +1454,18 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@formatjs/ecma402-abstract": { - "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.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { - "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" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "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.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "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.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", - "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", - "license": "MIT", - "dependencies": { - "tslib": "2" - } - }, - "node_modules/@hexagon/base64": { - "version": "1.1.28", - "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", - "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, "peerDependencies": { - "react-hook-form": "^7.55.0" + "react-hook-form": "^7.0.0" } }, "node_modules/@humanfs/core": { @@ -2535,18 +1478,31 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@humanwhocodes/retry": "^0.3.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", @@ -2561,9 +1517,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -2573,24 +1529,13 @@ "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.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2603,40 +1548,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2646,404 +1567,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.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": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.5.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3061,39 +1584,19 @@ "node": ">=12" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "minipass": "^7.0.4" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3106,28 +1609,27 @@ "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==", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "engines": { + "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3135,63 +1637,58 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@levischuck/tiny-cbor": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", - "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", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@next/env": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", - "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.4.tgz", - "integrity": "sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.3.tgz", + "integrity": "sha512-oeP1vnc5Cq9UoOb8SYHAEPbCXMzOgG70l+Zfd+Ie00R25FOm+CCVNrcIubJvB1tvBgakXE37MmqSycksXVPRqg==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, "node_modules/@next/swc-darwin-arm64": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", "cpu": [ "arm64" ], @@ -3205,9 +1702,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", - "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", "cpu": [ "x64" ], @@ -3221,9 +1718,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", "cpu": [ "arm64" ], @@ -3237,9 +1734,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", "cpu": [ "arm64" ], @@ -3253,9 +1750,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", "cpu": [ "x64" ], @@ -3269,9 +1766,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", "cpu": [ "x64" ], @@ -3285,9 +1782,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", "cpu": [ "arm64" ], @@ -3301,9 +1798,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "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==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", "cpu": [ "x64" ], @@ -3317,9 +1814,9 @@ } }, "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", "dev": true, "license": "MIT", "engines": { @@ -3330,13 +1827,13 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "1.7.1" }, "engines": { "node": "^14.21.3 || >=16" @@ -3346,9 +1843,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", "dev": true, "license": "MIT", "engines": { @@ -3384,9 +1881,9 @@ } }, "node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", - "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", + "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", "cpu": [ "arm" ], @@ -3400,9 +1897,9 @@ } }, "node_modules/@node-rs/argon2-android-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", - "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", + "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", "cpu": [ "arm64" ], @@ -3432,9 +1929,9 @@ } }, "node_modules/@node-rs/argon2-darwin-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", - "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", + "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", "cpu": [ "x64" ], @@ -3448,9 +1945,9 @@ } }, "node_modules/@node-rs/argon2-freebsd-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", - "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", "cpu": [ "x64" ], @@ -3464,9 +1961,9 @@ } }, "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", - "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", "cpu": [ "arm" ], @@ -3480,9 +1977,9 @@ } }, "node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", - "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", "cpu": [ "arm64" ], @@ -3496,9 +1993,9 @@ } }, "node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", - "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", "cpu": [ "arm64" ], @@ -3528,6 +2025,211 @@ } }, "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", + "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-linux-x64-musl": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", @@ -3543,7 +2245,7 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-wasm32-wasi": { + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-wasm32-wasi": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", @@ -3559,7 +2261,7 @@ "node": ">=14.0.0" } }, - "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-arm64-msvc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", @@ -3575,7 +2277,7 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-ia32-msvc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", @@ -3591,7 +2293,7 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-win32-x64-msvc": { + "node_modules/@node-rs/argon2/node_modules/@node-rs/argon2-win32-x64-msvc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", @@ -3636,38 +2338,6 @@ "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, - "node_modules/@node-rs/bcrypt-android-arm-eabi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", - "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-android-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", - "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt-darwin-arm64": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", @@ -3684,215 +2354,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/bcrypt-darwin-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", - "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-freebsd-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", - "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", - "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", - "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", - "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", - "integrity": "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz", - "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", - "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", - "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", - "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-x64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", - "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3983,161 +2444,12 @@ "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", "license": "MIT" }, - "node_modules/@peculiar/asn1-android": { - "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.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.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.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.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.5.0", - "@peculiar/asn1-x509": "^2.5.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-schema": { - "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", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "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.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/@petamoriken/float16": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", + "integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==", + "dev": true, + "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -4150,32 +2462,25 @@ "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", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", @@ -4193,16 +2498,15 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz", + "integrity": "sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4220,19 +2524,19 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz", + "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4250,19 +2554,19 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", + "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4280,15 +2584,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4306,9 +2610,9 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4321,9 +2625,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4336,25 +2640,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" }, "peerDependencies": { "@types/react": "*", @@ -4372,9 +2676,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4387,16 +2691,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4414,18 +2718,18 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz", + "integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4443,9 +2747,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4458,14 +2762,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4492,12 +2796,12 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4510,12 +2814,12 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", @@ -4533,29 +2837,29 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", + "integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" }, "peerDependencies": { "@types/react": "*", @@ -4573,26 +2877,26 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", + "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" }, "peerDependencies": { "@types/react": "*", @@ -4610,21 +2914,21 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4642,13 +2946,13 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4666,13 +2970,13 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4690,12 +2994,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4713,13 +3017,13 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz", + "integrity": "sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==", "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4736,22 +3040,93 @@ } } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-slot": "1.2.0" + }, + "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-progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", + "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4769,51 +3144,20 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@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-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4831,32 +3175,32 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", + "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" }, "peerDependencies": { "@types/react": "*", @@ -4874,12 +3218,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", + "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", @@ -4897,12 +3241,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4915,18 +3259,18 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4944,19 +3288,19 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", "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-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -4974,113 +3318,23 @@ } }, "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz", + "integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "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", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -5098,9 +3352,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5113,31 +3367,12 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -5150,30 +3385,12 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -5186,9 +3403,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5201,9 +3418,9 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5216,12 +3433,12 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -5234,12 +3451,12 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -5252,12 +3469,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", @@ -5275,24 +3492,24 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, "node_modules/@react-email/body": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.1.0.tgz", - "integrity": "sha512-o1bcSAmDYNNHECbkeyceCVPGmVsYvT+O3sSO/Ct7apKUu3JphTi31hu+0Nwqr/pgV5QFqdoT5vdS3SW5DJFHgQ==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", + "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==", "license": "MIT", "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/button": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", - "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz", + "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5302,12 +3519,12 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.1.0.tgz", - "integrity": "sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.12.tgz", + "integrity": "sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==", "license": "MIT", "dependencies": { - "prismjs": "^1.30.0" + "prismjs": "1.30.0" }, "engines": { "node": ">=18.0.0" @@ -5341,14 +3558,14 @@ } }, "node_modules/@react-email/components": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.6.tgz", - "integrity": "sha512-3o9ellDaF3bBcVMWeos9HI0iUIT1zGygPRcn9WSfI5JREORiN6ViEJIvz5SKWEn1KPNZtw/iaW8ct7PpVyhomg==", + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.36.tgz", + "integrity": "sha512-VMh+OQplAnG8JMLlJjdnjt+ThJZ+JVkp0q2YMS2NEz+T88N22bLD2p7DZO0QgtNaKgumOhJI/0a2Q7VzCrwu5g==", "license": "MIT", "dependencies": { - "@react-email/body": "0.1.0", - "@react-email/button": "0.2.0", - "@react-email/code-block": "0.1.0", + "@react-email/body": "0.0.11", + "@react-email/button": "0.0.19", + "@react-email/code-block": "0.0.12", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", "@react-email/container": "0.0.15", @@ -5359,13 +3576,13 @@ "@react-email/html": "0.0.11", "@react-email/img": "0.0.11", "@react-email/link": "0.0.12", - "@react-email/markdown": "0.0.15", - "@react-email/preview": "0.0.13", - "@react-email/render": "1.3.2", + "@react-email/markdown": "0.0.14", + "@react-email/preview": "0.0.12", + "@react-email/render": "1.0.6", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.2.2", - "@react-email/text": "0.1.5" + "@react-email/tailwind": "1.0.4", + "@react-email/text": "0.1.1" }, "engines": { "node": ">=18.0.0" @@ -5468,12 +3685,12 @@ } }, "node_modules/@react-email/markdown": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.15.tgz", - "integrity": "sha512-UQA9pVm5sbflgtg3EX3FquUP4aMBzmLReLbGJ6DZQZnAskBF36aI56cRykDq1o+1jT+CKIK1CducPYziaXliag==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz", + "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==", "license": "MIT", "dependencies": { - "md-to-react-email": "^5.0.5" + "md-to-react-email": "5.0.5" }, "engines": { "node": ">=18.0.0" @@ -5483,9 +3700,9 @@ } }, "node_modules/@react-email/preview": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", - "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz", + "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5494,539 +3711,15 @@ "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.3.2", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.3.2.tgz", - "integrity": "sha512-oq8/BD/I/YspeuBjjdLJG6xaf9tsPYk+VWu8/mX9xWbRN0t0ExKSVm9sEBL6RsCpndQA2jbY2VgPEreIrzUgqw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", + "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", "license": "MIT", "dependencies": { - "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" + "html-to-text": "9.0.5", + "prettier": "3.5.3", + "react-promise-suspense": "0.3.4" }, "engines": { "node": ">=18.0.0" @@ -6061,9 +3754,9 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.2.2.tgz", - "integrity": "sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz", + "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6073,9 +3766,9 @@ } }, "node_modules/@react-email/text": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", - "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.1.tgz", + "integrity": "sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6091,9 +3784,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "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==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz", + "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", "license": "MIT" }, "node_modules/@scarf/scarf": { @@ -6103,12 +3796,6 @@ "hasInstallScript": true, "license": "Apache-2.0" }, - "node_modules/@schummar/icu-type-parser": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", - "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", - "license": "MIT" - }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -6122,660 +3809,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/@simplewebauthn/browser": { - "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": "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", - "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.3.10", - "@peculiar/asn1-ecc": "^2.3.8", - "@peculiar/asn1-rsa": "^2.3.8", - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/asn1-x509": "^2.3.8", - "@peculiar/x509": "^1.13.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@smithy/abort-controller": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "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.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/@smithy/core": { - "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.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/credential-provider-imds": { - "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.3.2", - "@smithy/property-provider": "^4.2.2", - "@smithy/types": "^4.7.1", - "@smithy/url-parser": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "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.3.2", - "@smithy/querystring-builder": "^4.2.2", - "@smithy/types": "^4.7.1", - "@smithy/util-base64": "^4.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "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.7.1", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "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.3.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "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.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": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "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.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-serde": { - "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.3.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "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.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/@smithy/node-http-handler": { - "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.2.2", - "@smithy/protocol-http": "^5.3.2", - "@smithy/querystring-builder": "^4.2.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "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.7.1", - "@smithy/util-uri-escape": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "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.7.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "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.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": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "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.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": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "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.2.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "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.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "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.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "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.2.2", - "@smithy/smithy-client": "^4.8.1", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "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.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": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "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.3.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "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.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "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.2.2", - "@smithy/types": "^4.7.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "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.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": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "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": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "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.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", @@ -6783,11 +3816,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { "version": "0.5.15", @@ -6811,54 +3844,46 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", - "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz", + "integrity": "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.0", - "lightningcss": "1.30.1", - "magic-string": "^0.30.19", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.14" + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "tailwindcss": "4.1.4" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", - "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.4.tgz", + "integrity": "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.5.1" - }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@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" + "@tailwindcss/oxide-android-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-x64": "4.1.4", + "@tailwindcss/oxide-freebsd-x64": "4.1.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-x64-musl": "4.1.4", + "@tailwindcss/oxide-wasm32-wasi": "4.1.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", - "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.4.tgz", + "integrity": "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==", "cpu": [ "arm64" ], @@ -6873,9 +3898,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", - "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.4.tgz", + "integrity": "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==", "cpu": [ "arm64" ], @@ -6890,9 +3915,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", - "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.4.tgz", + "integrity": "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==", "cpu": [ "x64" ], @@ -6907,9 +3932,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", - "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.4.tgz", + "integrity": "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==", "cpu": [ "x64" ], @@ -6924,9 +3949,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.4.tgz", + "integrity": "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==", "cpu": [ "arm" ], @@ -6941,9 +3966,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.4.tgz", + "integrity": "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==", "cpu": [ "arm64" ], @@ -6958,9 +3983,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.4.tgz", + "integrity": "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==", "cpu": [ "arm64" ], @@ -6975,9 +4000,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.4.tgz", + "integrity": "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==", "cpu": [ "x64" ], @@ -6992,9 +4017,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.4.tgz", + "integrity": "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==", "cpu": [ "x64" ], @@ -7009,9 +4034,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.4.tgz", + "integrity": "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -7027,21 +4052,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@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" + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@emnapi/wasi-threads": "^1.0.1", + "@napi-rs/wasm-runtime": "^0.2.8", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", - "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.4.tgz", + "integrity": "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==", "cpu": [ "arm64" ], @@ -7056,9 +4081,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "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==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.4.tgz", + "integrity": "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==", "cpu": [ "x64" ], @@ -7073,26 +4098,26 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", - "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.4.tgz", + "integrity": "sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.14", - "@tailwindcss/oxide": "4.1.14", + "@tailwindcss/node": "4.1.4", + "@tailwindcss/oxide": "4.1.4", "postcss": "^8.4.41", - "tailwindcss": "4.1.14" + "tailwindcss": "4.1.4" } }, "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "version": "8.20.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", + "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.21.3" + "@tanstack/table-core": "8.20.5" }, "engines": { "node": ">=12" @@ -7107,9 +4132,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", "license": "MIT", "engines": { "node": ">=12" @@ -7120,9 +4145,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", "license": "MIT", "optional": true, "dependencies": { @@ -7140,9 +4165,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "license": "MIT", "dependencies": { @@ -7161,9 +4186,9 @@ } }, "node_modules/@types/cookie-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", - "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -7171,9 +4196,9 @@ } }, "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "license": "MIT", "dependencies": { @@ -7187,50 +4212,29 @@ "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", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "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==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", "dev": true, "license": "MIT", "dependencies": { @@ -7240,20 +4244,10 @@ "@types/send": "*" } }, - "node_modules/@types/express-session": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", - "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true, "license": "MIT" }, @@ -7284,9 +4278,9 @@ "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7309,56 +4303,29 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", - "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~6.20.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz", - "integrity": "sha512-Zo6uOA9157WRgBk/ZhMpTQ/iCWLMk7OIs/Q9jvHarMvrzUUP/MDdPHL2U1zpf57HrrWGv4nYQn5uIxna0xY3xw==", + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", "dev": true, "license": "MIT", "dependencies": { - "@aws-sdk/client-sesv2": "^3.839.0", "@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", - "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "dev": true, "license": "MIT" }, @@ -7370,9 +4337,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", + "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7380,48 +4347,26 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.2.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true, "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==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "license": "MIT", "dependencies": { @@ -7429,6 +4374,18 @@ "@types/node": "*" } }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "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", @@ -7446,22 +4403,10 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "license": "MIT", "dependencies": { @@ -7486,20 +4431,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.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", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7509,30 +4454,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", - "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", "license": "MIT", "dependencies": { - "@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", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4" }, "engines": { @@ -7544,38 +4480,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "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.46.1", - "@typescript-eslint/types": "^8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7585,33 +4500,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", - "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7622,13 +4520,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7639,21 +4537,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", "license": "MIT", "dependencies": { - "@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", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7663,13 +4559,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7719,15 +4615,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", - "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7738,17 +4634,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.21.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7758,448 +4654,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@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", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -8208,19 +4679,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -8246,52 +4704,10 @@ "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.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -8315,13 +4731,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -8350,9 +4759,9 @@ } }, "node_modules/arctic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/arctic/-/arctic-3.7.0.tgz", - "integrity": "sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/arctic/-/arctic-3.6.0.tgz", + "integrity": "sha512-egHDsCqEacb6oSHz5QSSxNhp07J+QJwJdPvs0katL+mNM5LaGQVqxmcdq1KwfaSNSAlVumBBs0MRExS88TxbMg==", "license": "MIT", "dependencies": { "@oslojs/crypto": "1.0.1", @@ -8360,13 +4769,6 @@ "@oslojs/jwt": "0.2.0" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8374,9 +4776,9 @@ "license": "Python-2.0" }, "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -8410,20 +4812,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -8432,6 +4838,18 @@ "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", @@ -8463,18 +4881,17 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", + "es-abstract": "^1.23.2", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8556,20 +4973,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1js": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", - "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", - "license": "BSD-3-Clause", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -8597,44 +5000,6 @@ "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", @@ -8651,22 +5016,22 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "license": "MPL-2.0", "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, @@ -8715,16 +5080,6 @@ "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", @@ -8770,43 +5125,48 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "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/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } }, - "node_modules/bowser": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", - "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", - "dev": true, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8825,40 +5185,6 @@ "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", @@ -8896,6 +5222,17 @@ "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -8924,9 +5261,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8937,13 +5274,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -8961,20 +5298,10 @@ "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.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "funding": [ { "type": "opencollective", @@ -9034,34 +5361,10 @@ } }, "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "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", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -9108,75 +5411,88 @@ "license": "MIT" }, "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "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" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/cliui/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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/cliui/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==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, "node_modules/cliui/node_modules/wrap-ansi": { - "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==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "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" @@ -9192,15 +5508,15 @@ } }, "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", @@ -9208,16 +5524,17 @@ } }, "node_modules/color": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", - "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", + "optional": true, "dependencies": { - "color-convert": "^3.0.1", - "color-string": "^2.0.0" + "color-convert": "^2.0.1", + "color-string": "^1.9.0" }, "engines": { - "node": ">=18" + "node": ">=12.5.0" } }, "node_modules/color-convert": { @@ -9239,47 +5556,50 @@ "license": "MIT" }, "node_modules/color-string": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", - "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, - "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==", + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", "license": "MIT", "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" + "color": "^3.1.3", + "text-hex": "1.0.x" } }, - "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==", + "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==", "license": "MIT", - "engines": { - "node": ">=12.20" + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, + "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", @@ -9308,27 +5628,10 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -9346,13 +5649,6 @@ "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", @@ -9457,49 +5753,6 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -9574,9 +5827,9 @@ } }, "node_modules/debounce": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", - "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", + "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==", "dev": true, "license": "MIT", "engines": { @@ -9587,9 +5840,9 @@ } }, "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9603,12 +5856,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -9661,16 +5908,6 @@ "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", @@ -9723,10 +5960,20 @@ "node": ">= 0.8" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -9738,13 +5985,6 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -9758,13 +5998,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -9833,9 +6066,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -9846,25 +6079,456 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.5", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.5.tgz", - "integrity": "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==", + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", + "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", "dev": true, "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.25.4", - "esbuild-register": "^3.5.0" + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/drizzle-orm": { - "version": "0.44.6", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.6.tgz", - "integrity": "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ==", + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.38.3.tgz", + "integrity": "sha512-w41Y+PquMpSff/QDRGdItG0/aWca+/J3Sda9PPGkTxBtjWQvgU1jxlFBXdjog5tYvTu58uvi3PwR1NuCx0KeZg==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -9875,24 +6539,24 @@ "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1.13", + "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", + "@types/react": ">=18", "@types/sql.js": "*", - "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", - "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", + "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, @@ -9936,10 +6600,10 @@ "@types/pg": { "optional": true }, - "@types/sql.js": { + "@types/react": { "optional": true }, - "@upstash/redis": { + "@types/sql.js": { "optional": true }, "@vercel/postgres": { @@ -9957,9 +6621,6 @@ "expo-sqlite": { "optional": true }, - "gel": { - "optional": true - }, "knex": { "optional": true }, @@ -9978,6 +6639,9 @@ "prisma": { "optional": true }, + "react": { + "optional": true + }, "sql.js": { "optional": true }, @@ -10016,16 +6680,16 @@ } }, "node_modules/eciesjs": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", - "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz", + "integrity": "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==", "dev": true, "license": "MIT", "dependencies": { - "@ecies/ciphers": "^0.2.4", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" + "@ecies/ciphers": "^0.2.2", + "@noble/ciphers": "^1.0.0", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0" }, "engines": { "bun": ">=1", @@ -10039,13 +6703,6 @@ "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", @@ -10068,9 +6725,9 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -10097,60 +6754,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -10161,20 +6764,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -10203,39 +6792,6 @@ } } }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/engine.io/node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -10259,10 +6815,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10284,28 +6839,41 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", + "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -10317,24 +6885,21 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", - "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", + "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", + "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -10343,7 +6908,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -10397,13 +6962,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -10432,15 +6990,12 @@ } }, "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -10461,9 +7016,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10474,32 +7029,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/esbuild-node-externals": { @@ -10535,6 +7089,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10559,22 +7114,21 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -10582,9 +7136,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -10619,12 +7173,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.4.tgz", - "integrity": "sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.3.tgz", + "integrity": "sha512-wGYlNuWnh4ujuKtZvH+7B2Z2vy9nONZE6ztd+DKF7hAsIabkrxmD4TzYHzASHENo42lmz2tnT2B+zN2sOHvpJg==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.4", + "@next/eslint-plugin-next": "15.1.3", "@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", @@ -10666,24 +7220,25 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" }, "peerDependencies": { "eslint": "*", @@ -10699,10 +7254,38 @@ } } }, + "node_modules/eslint-import-resolver-typescript/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/eslint-import-resolver-typescript/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "license": "MIT", "dependencies": { "debug": "^3.2.7" @@ -10726,29 +7309,29 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", - "is-core-module": "^2.16.1", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.1", + "object.values": "^1.2.0", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -10806,9 +7389,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "license": "MIT", "dependencies": { "array-includes": "^3.1.8", @@ -10821,7 +7404,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.9", + "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -10838,9 +7421,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "license": "MIT", "engines": { "node": ">=10" @@ -10876,9 +7459,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -10892,9 +7475,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10904,14 +7487,14 @@ } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10971,16 +7554,6 @@ "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", @@ -11015,41 +7588,45 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", @@ -11057,13 +7634,10 @@ } }, "node_modules/express-rate-limit": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", - "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "license": "MIT", - "dependencies": { - "ip-address": "10.0.1" - }, "engines": { "node": ">= 16" }, @@ -11071,32 +7645,31 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": ">= 4.11" + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "engines": { - "node": ">=6.6.0" + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "dev": true, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -11145,59 +7718,21 @@ "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", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -11276,22 +7811,38 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -11322,9 +7873,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "license": "ISC" }, "node_modules/fn.name": { @@ -11334,9 +7885,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -11354,9 +7905,9 @@ } }, "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -11369,12 +7920,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" }, "engines": { @@ -11397,42 +7948,19 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -11454,55 +7982,13 @@ "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", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/fs-constants": { @@ -11512,9 +7998,9 @@ "license": "MIT" }, "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "license": "Unlicense", "optional": true }, @@ -11571,58 +8057,49 @@ "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==", + "node_modules/gel": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/gel/-/gel-2.0.2.tgz", + "integrity": "sha512-XTKpfNR9HZOw+k0Bl04nETZjuP5pypVAXsZADSdwr3EtyygTTe1RqvftU2FjGu7Tp9e576a9b/iIOxWrRBxMiQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@petamoriken/float16": "^3.8.7", + "debug": "^4.3.4", + "env-paths": "^3.0.0", + "semver": "^7.6.2", + "shell-quote": "^1.8.1", + "which": "^4.0.0" + }, + "bin": { + "gel": "dist/cli.mjs" + }, "engines": { - "node": ">=6.9.0" + "node": ">= 18.0.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", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", + "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "get-proto": "^1.0.1", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -11688,9 +8165,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -11706,14 +8183,14 @@ "license": "MIT" }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "license": "ISC", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -11740,20 +8217,22 @@ "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/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "license": "ISC", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^2.0.1" }, "engines": { "node": "20 || >=22" @@ -11763,13 +8242,15 @@ } }, "node_modules/globals": { - "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, + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -11825,7 +8306,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -11921,20 +8401,10 @@ "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", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", + "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -11991,15 +8461,6 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -12019,12 +8480,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" @@ -12060,9 +8521,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -12097,9 +8558,9 @@ "license": "ISC" }, "node_modules/input-otp": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", - "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", + "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", @@ -12120,27 +8581,6 @@ "node": ">= 0.4" } }, - "node_modules/intl-messageformat": { - "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.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -12167,6 +8607,12 @@ "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", @@ -12215,12 +8661,12 @@ } }, "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", + "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" }, "engines": { @@ -12231,12 +8677,12 @@ } }, "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", "license": "MIT", "dependencies": { - "semver": "^7.7.1" + "semver": "^7.6.3" } }, "node_modules/is-callable": { @@ -12333,14 +8779,13 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -12385,18 +8830,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12422,12 +8855,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -12559,12 +8986,12 @@ } }, "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "call-bound": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -12623,9 +9050,9 @@ } }, "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -12637,41 +9064,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "devOptional": true, "license": "MIT", "bin": { @@ -12724,13 +9120,6 @@ "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", @@ -12743,19 +9132,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, - "node_modules/json5": { - "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", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -12794,12 +9170,12 @@ } }, "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", + "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -12835,16 +9211,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -12892,9 +9258,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -12908,22 +9274,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", "cpu": [ "arm64" ], @@ -12942,9 +9308,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", "cpu": [ "x64" ], @@ -12963,9 +9329,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", "cpu": [ "x64" ], @@ -12984,9 +9350,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", "cpu": [ "arm" ], @@ -13005,9 +9371,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", "cpu": [ "arm64" ], @@ -13026,9 +9392,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", "cpu": [ "arm64" ], @@ -13047,9 +9413,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", "cpu": [ "x64" ], @@ -13068,9 +9434,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", "cpu": [ "x64" ], @@ -13089,9 +9455,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", "cpu": [ "arm64" ], @@ -13110,9 +9476,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", "cpu": [ "x64" ], @@ -13130,37 +9496,6 @@ "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", @@ -13271,34 +9606,23 @@ } }, "node_modules/lru-cache": { - "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, + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "engines": { + "node": "20 || >=22" } }, "node_modules/lucide-react": { - "version": "0.545.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", - "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "version": "0.469.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", + "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/marked": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", @@ -13320,20 +9644,6 @@ "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", @@ -13347,12 +9657,12 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/memfs": { @@ -13379,13 +9689,10 @@ } }, "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -13406,6 +9713,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -13431,22 +9747,34 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -13462,19 +9790,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -13526,46 +9841,12 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib": { - "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": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "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", @@ -13575,23 +9856,6 @@ "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", @@ -13612,22 +9876,10 @@ "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", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -13648,21 +9900,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/napi-postinstall": { - "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" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -13670,29 +9907,24 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "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.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", - "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.4", + "@next/env": "15.2.4", + "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", + "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -13704,19 +9936,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@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" + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", + "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", + "@playwright/test": "^1.41.2", "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", @@ -13737,37 +9969,10 @@ } } }, - "node_modules/next-intl": { - "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", - "url": "https://github.com/sponsors/amannn" - } - ], - "license": "MIT", - "dependencies": { - "@formatjs/intl-localematcher": "^0.5.4", - "negotiator": "^1.0.0", - "use-intl": "^4.3.12" - }, - "peerDependencies": { - "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", + "integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==", "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", @@ -13803,9 +10008,9 @@ } }, "node_modules/node-abi": { - "version": "3.78.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", - "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "version": "3.73.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.73.0.tgz", + "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -13826,11 +10031,19 @@ "node": ">= 8.0.0" } }, + "node_modules/node-cache/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -13864,28 +10077,10 @@ "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.9", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -13901,20 +10096,10 @@ "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.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.2.tgz", - "integrity": "sha512-7iKzNfy8lWYs3zq4oFPa8EXZz5xt9gQNKJZau3B1ErLBb6bF7sBJ00x09485DOvRT2l5Gerbl3VlZNT57MxJVA==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.2.0.tgz", + "integrity": "sha512-PcnFC6gTo9VDkxVaQ1/mZAS3JoWrDjAI+a6e2NgfYQSGDwftJlbdV0jBMi2V8xQPqbGcWaa7p3UP0SKF+Bhm2g==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -13958,6 +10143,7 @@ "ms", "node-gyp", "nopt", + "normalize-package-data", "npm-audit-report", "npm-install-checks", "npm-package-arg", @@ -13992,69 +10178,70 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.6", - "@npmcli/config": "^10.4.2", + "@npmcli/arborist": "^9.0.1", + "@npmcli/config": "^10.1.0", "@npmcli/fs": "^4.0.0", - "@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": "^10.0.0", - "@sigstore/tuf": "^4.0.0", - "abbrev": "^3.0.1", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.1.1", + "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/redact": "^3.1.1", + "@npmcli/run-script": "^9.0.1", + "@sigstore/tuf": "^3.0.0", + "abbrev": "^3.0.0", "archy": "~1.0.0", - "cacache": "^20.0.1", - "chalk": "^5.6.2", - "ci-info": "^4.3.1", + "cacache": "^19.0.1", + "chalk": "^5.4.1", + "ci-info": "^4.1.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^11.0.3", + "glob": "^10.4.5", "graceful-fs": "^4.2.11", - "hosted-git-info": "^9.0.2", + "hosted-git-info": "^8.0.2", "ini": "^5.0.0", - "init-package-json": "^8.2.2", - "is-cidr": "^6.0.1", + "init-package-json": "^8.0.0", + "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", - "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", + "libnpmaccess": "^10.0.0", + "libnpmdiff": "^8.0.1", + "libnpmexec": "^10.1.0", + "libnpmfund": "^7.0.1", + "libnpmorg": "^8.0.0", + "libnpmpack": "^9.0.1", + "libnpmpublish": "^11.0.0", + "libnpmsearch": "^9.0.0", + "libnpmteam": "^8.0.0", + "libnpmversion": "^8.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.5", "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^11.4.2", + "node-gyp": "^11.1.0", "nopt": "^8.1.0", + "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", - "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-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-user-validate": "^3.0.0", "p-map": "^7.0.3", - "pacote": "^21.0.3", + "pacote": "^21.0.0", "parse-conflict-json": "^4.0.0", "proc-log": "^5.0.0", "qrcode-terminal": "^0.12.0", "read": "^4.1.0", - "semver": "^7.7.3", + "semver": "^7.7.1", "spdx-expression-parse": "^4.0.0", "ssri": "^12.0.0", - "supports-color": "^10.2.2", - "tar": "^7.5.1", + "supports-color": "^10.0.0", + "tar": "^6.2.1", "text-table": "~0.2.0", - "tiny-relative-date": "^2.0.2", + "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.2", + "validate-npm-package-name": "^6.0.0", "which": "^5.0.0" }, "bin": { @@ -14078,25 +10265,6 @@ "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, @@ -14114,7 +10282,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", + "version": "6.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -14146,7 +10314,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.1.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -14176,54 +10344,55 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { - "version": "4.0.0", + "version": "3.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": "^11.2.1", + "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.6", + "version": "9.0.1", "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": "^5.0.0", - "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^9.0.0", "@npmcli/name-from-folder": "^3.0.0", "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^7.0.0", + "@npmcli/package-json": "^6.0.1", "@npmcli/query": "^4.0.0", "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^10.0.0", + "@npmcli/run-script": "^9.0.1", "bin-links": "^5.0.0", - "cacache": "^20.0.1", + "cacache": "^19.0.1", "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^9.0.0", + "hosted-git-info": "^8.0.0", "json-stringify-nice": "^1.1.4", - "lru-cache": "^11.2.1", - "minimatch": "^10.0.3", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", "nopt": "^8.0.0", "npm-install-checks": "^7.1.0", - "npm-package-arg": "^13.0.0", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "pacote": "^21.0.2", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^21.0.0", "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", @@ -14237,12 +10406,12 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.2", + "version": "10.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/map-workspaces": "^5.0.0", - "@npmcli/package-json": "^7.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", "ci-info": "^4.0.0", "ini": "^5.0.0", "nopt": "^8.1.0", @@ -14266,21 +10435,21 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "7.0.0", + "version": "6.0.3", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^8.0.0", "ini": "^5.0.0", - "lru-cache": "^11.2.1", - "npm-pick-manifest": "^11.0.1", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^5.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { @@ -14299,25 +10468,25 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "5.0.0", + "version": "4.0.2", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^7.0.0", - "glob": "^11.0.3", - "minimatch": "^10.0.3" + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.2", + "version": "9.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "cacache": "^20.0.0", + "cacache": "^19.0.0", "json-parse-even-better-errors": "^4.0.0", "pacote": "^21.0.0", "proc-log": "^5.0.0", @@ -14344,24 +10513,24 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "7.0.1", + "version": "6.1.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^7.0.0", - "glob": "^11.0.3", - "hosted-git-info": "^9.0.0", + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.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": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.3", + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -14372,18 +10541,18 @@ } }, "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.1", + "version": "4.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "postcss-selector-parser": "^6.1.2" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.2.2", + "version": "3.1.1", "inBundle": true, "license": "ISC", "engines": { @@ -14391,19 +10560,19 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "10.0.0", + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^7.0.0", + "@npmcli/package-json": "^6.0.0", "@npmcli/promise-spawn": "^8.0.0", "node-gyp": "^11.0.0", "proc-log": "^5.0.0", "which": "^5.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@pkgjs/parseargs": { @@ -14416,26 +10585,26 @@ } }, "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "4.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", + "version": "2.0.0", "inBundle": true, "license": "Apache-2.0", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", + "version": "0.4.0", "inBundle": true, "license": "Apache-2.0", "engines": { @@ -14443,44 +10612,44 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", "proc-log": "^5.0.0", "promise-retry": "^2.0.1" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" + "@sigstore/protobuf-specs": "^0.4.0", + "tuf-js": "^3.0.1" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", + "version": "2.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0" + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@tufjs/canonical-json": { @@ -14492,7 +10661,7 @@ } }, "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -14500,25 +10669,11 @@ "minimatch": "^9.0.5" }, "engines": { - "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": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/abbrev": { - "version": "3.0.1", + "version": "3.0.0", "inBundle": true, "license": "ISC", "engines": { @@ -14526,7 +10681,7 @@ } }, "node_modules/npm/node_modules/agent-base": { - "version": "7.1.4", + "version": "7.1.3", "inBundle": true, "license": "MIT", "engines": { @@ -14542,7 +10697,7 @@ } }, "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.3", + "version": "6.2.1", "inBundle": true, "license": "MIT", "engines": { @@ -14553,7 +10708,7 @@ } }, "node_modules/npm/node_modules/aproba": { - "version": "2.1.0", + "version": "2.0.0", "inBundle": true, "license": "ISC" }, @@ -14583,7 +10738,7 @@ } }, "node_modules/npm/node_modules/binary-extensions": { - "version": "3.1.0", + "version": "3.0.0", "inBundle": true, "license": "MIT", "engines": { @@ -14594,7 +10749,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -14602,28 +10757,87 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "20.0.1", + "version": "19.0.1", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^11.0.3", - "lru-cache": "^11.1.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": "^20.17.0 || >=22.9.0" + "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.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "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_modules/npm/node_modules/chalk": { - "version": "5.6.2", + "version": "5.4.1", "inBundle": true, "license": "MIT", "engines": { @@ -14634,15 +10848,15 @@ } }, "node_modules/npm/node_modules/chownr": { - "version": "3.0.0", + "version": "2.0.0", "inBundle": true, - "license": "BlueOak-1.0.0", + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=10" } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.3.1", + "version": "4.1.0", "funding": [ { "type": "github", @@ -14656,14 +10870,14 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.1", + "version": "4.1.3", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "ip-regex": "5.0.0" + "ip-regex": "^5.0.0" }, "engines": { - "node": ">=20" + "node": ">=14" } }, "node_modules/npm/node_modules/cli-columns": { @@ -14720,11 +10934,6 @@ "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, @@ -14751,7 +10960,7 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.4.3", + "version": "4.4.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -14767,7 +10976,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.2", + "version": "7.0.0", "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -14846,23 +11055,20 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "11.0.3", + "version": "10.4.5", "inBundle": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "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": "^2.0.0" + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": "20 || >=22" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -14873,18 +11079,18 @@ "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { - "version": "9.0.2", + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "lru-cache": "^11.1.0" + "lru-cache": "^10.0.1" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.2.0", + "version": "4.1.1", "inBundle": true, "license": "BSD-2-Clause" }, @@ -14925,14 +11131,14 @@ } }, "node_modules/npm/node_modules/ignore-walk": { - "version": "8.0.0", + "version": "7.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "minimatch": "^10.0.3" + "minimatch": "^9.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/imurmurhash": { @@ -14952,26 +11158,30 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "8.2.2", + "version": "8.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/package-json": "^7.0.0", - "npm-package-arg": "^13.0.0", + "@npmcli/package-json": "^6.1.0", + "npm-package-arg": "^12.0.0", "promzard": "^2.0.0", "read": "^4.0.0", - "semver": "^7.7.2", + "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.2" + "validate-npm-package-name": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", + "version": "9.0.5", "inBundle": true, "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, "engines": { "node": ">= 12" } @@ -14988,14 +11198,14 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.1", + "version": "5.1.1", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "5.0.1" + "cidr-regex": "^4.1.1" }, "engines": { - "node": ">=20" + "node": ">=14" } }, "node_modules/npm/node_modules/is-fullwidth-code-point": { @@ -15007,27 +11217,29 @@ } }, "node_modules/npm/node_modules/isexe": { - "version": "3.1.1", + "version": "2.0.0", "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } + "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { - "version": "4.1.1", + "version": "3.4.3", "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, @@ -15063,51 +11275,50 @@ "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.3", + "version": "10.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0" + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.9", + "version": "8.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.6", + "@npmcli/arborist": "^9.0.1", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", - "diff": "^8.0.2", - "minimatch": "^10.0.3", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", - "tar": "^7.5.1" + "diff": "^7.0.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", + "tar": "^6.2.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.8", + "version": "10.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.6", - "@npmcli/package-json": "^7.0.0", - "@npmcli/run-script": "^10.0.0", + "@npmcli/arborist": "^9.0.1", + "@npmcli/package-json": "^6.1.1", + "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", "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": { @@ -15115,54 +11326,54 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.9", + "version": "7.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.6" + "@npmcli/arborist": "^9.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.1", + "version": "8.0.0", "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" + "npm-registry-fetch": "^18.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.9", + "version": "9.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.6", - "@npmcli/run-script": "^10.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2" + "@npmcli/arborist": "^9.0.1", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.1.2", + "version": "11.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", "proc-log": "^5.0.0", "semver": "^7.3.7", - "sigstore": "^4.0.0", + "sigstore": "^3.0.0", "ssri": "^12.0.0" }, "engines": { @@ -15170,35 +11381,35 @@ } }, "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.1", + "version": "9.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^19.0.0" + "npm-registry-fetch": "^18.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.2", + "version": "8.0.0", "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" + "npm-registry-fetch": "^18.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.2", + "version": "8.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/run-script": "^10.0.0", + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", "json-parse-even-better-errors": "^4.0.0", "proc-log": "^5.0.0", "semver": "^7.3.7" @@ -15208,20 +11419,17 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", + "version": "10.4.3", "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } + "license": "ISC" }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.2", + "version": "14.0.3", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^4.0.0", - "cacache": "^20.0.1", + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", @@ -15233,18 +11441,26 @@ "ssri": "^12.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "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_modules/npm/node_modules/minimatch": { - "version": "10.0.3", + "version": "9.0.5", "inBundle": true, "license": "ISC", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15270,7 +11486,7 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.1", + "version": "4.0.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -15285,6 +11501,18 @@ "encoding": "^0.1.13" } }, + "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", "inBundle": true, @@ -15352,14 +11580,37 @@ } }, "node_modules/npm/node_modules/minizlib": { - "version": "3.1.0", + "version": "2.1.2", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "minipass": "^3.0.0", + "yallist": "^4.0.0" }, "engines": { - "node": ">= 18" + "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_modules/npm/node_modules/ms": { @@ -15375,28 +11626,20 @@ "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.4.2", + "version": "11.1.0", "inBundle": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", - "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { @@ -15406,129 +11649,62 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { "version": "3.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", - "socks-proxy-agent": "^8.0.3" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "19.0.1", + "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.1", "inBundle": true, - "license": "ISC", + "license": "MIT", "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" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/glob": { - "version": "10.4.5", + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", "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" - }, + "license": "MIT", "bin": { - "glob": "dist/esm/bin.mjs" + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "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", + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", "inBundle": true, "license": "ISC", "dependencies": { - "@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" + "@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.17.0 || >=20.5.0" + "node": ">=18" } }, - "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", + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.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" - }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, "node_modules/npm/node_modules/nopt": { @@ -15545,6 +11721,19 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.0", + "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, @@ -15565,7 +11754,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.2", + "version": "7.1.1", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -15584,73 +11773,84 @@ } }, "node_modules/npm/node_modules/npm-package-arg": { - "version": "13.0.1", + "version": "12.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "hosted-git-info": "^9.0.0", + "hosted-git-info": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^6.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.2", + "version": "10.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^5.0.0" + "ignore-walk": "^7.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "11.0.1", + "version": "10.0.0", "inBundle": true, "license": "ISC", "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^13.0.0", + "npm-package-arg": "^12.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/npm-profile": { - "version": "12.0.0", + "version": "11.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^19.0.0", + "npm-registry-fetch": "^18.0.0", "proc-log": "^5.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "19.0.0", + "version": "18.0.2", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/redact": "^3.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^15.0.0", + "make-fetch-happen": "^14.0.0", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minizlib": "^3.0.1", - "npm-package-arg": "^13.0.0", + "npm-package-arg": "^12.0.0", "proc-log": "^5.0.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" } }, "node_modules/npm/node_modules/npm-user-validate": { @@ -15678,27 +11878,27 @@ "license": "BlueOak-1.0.0" }, "node_modules/npm/node_modules/pacote": { - "version": "21.0.3", + "version": "21.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^7.0.0", + "@npmcli/git": "^6.0.0", "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^7.0.0", + "@npmcli/package-json": "^6.0.0", "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^10.0.0", - "cacache": "^20.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^13.0.0", - "npm-packlist": "^10.0.1", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^10.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", + "sigstore": "^3.0.0", "ssri": "^12.0.0", - "tar": "^7.4.3" + "tar": "^6.1.11" }, "bin": { "pacote": "bin/index.js" @@ -15729,22 +11929,22 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", + "version": "1.11.1", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "6.1.2", "inBundle": true, "license": "MIT", "dependencies": { @@ -15836,6 +12036,18 @@ "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, @@ -15844,6 +12056,20 @@ "node": ">= 4" } }, + "node_modules/npm/node_modules/rimraf": { + "version": "5.0.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", "inBundle": true, @@ -15851,7 +12077,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.7.3", + "version": "7.7.1", "inBundle": true, "license": "ISC", "bin": { @@ -15892,19 +12118,19 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@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" + "@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" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/smart-buffer": { @@ -15917,11 +12143,11 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.7", + "version": "2.8.4", "inBundle": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { @@ -15975,10 +12201,15 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.21", "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, @@ -16041,7 +12272,7 @@ } }, "node_modules/npm/node_modules/supports-color": { - "version": "10.2.2", + "version": "10.0.0", "inBundle": true, "license": "MIT", "engines": { @@ -16052,26 +12283,49 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.1", + "version": "6.2.1", "inBundle": true, "license": "ISC", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" + "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" }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/npm/node_modules/tar/node_modules/yallist": { + "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": { "version": "5.0.0", "inBundle": true, - "license": "BlueOak-1.0.0", + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=8" } }, "node_modules/npm/node_modules/text-table": { @@ -16080,52 +12334,10 @@ "license": "MIT" }, "node_modules/npm/node_modules/tiny-relative-date": { - "version": "2.0.2", + "version": "1.3.0", "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", "inBundle": true, @@ -16135,16 +12347,16 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/unique-filename": { @@ -16193,7 +12405,7 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.2", + "version": "6.0.0", "inBundle": true, "license": "ISC", "engines": { @@ -16222,6 +12434,14 @@ "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, @@ -16270,7 +12490,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", + "version": "6.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -16302,7 +12522,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.1.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -16332,39 +12552,6 @@ "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", - "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "pathe": "^2.0.3", - "pkg-types": "^2.0.0", - "tinyexec": "^0.3.2" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -16384,9 +12571,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -16435,15 +12622,14 @@ } }, "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -16546,12 +12732,12 @@ } }, "node_modules/openapi3-ts": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", - "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", "license": "MIT", "dependencies": { - "yaml": "^2.8.0" + "yaml": "^2.5.0" } }, "node_modules/optimist": { @@ -16638,26 +12824,6 @@ "@node-rs/bcrypt": "1.9.0" } }, - "node_modules/oslo/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/oslo/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/oslo/node_modules/@node-rs/argon2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", @@ -16683,38 +12849,6 @@ "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", - "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", - "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", @@ -16731,86 +12865,6 @@ "node": ">= 10" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", - "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", - "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", - "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", @@ -16827,99 +12881,6 @@ "node": ">= 10" } }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", - "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", - "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", - "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", - "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -17047,24 +13008,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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": "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" - } + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -17076,13 +13024,6 @@ "node": ">=8" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", @@ -17092,95 +13033,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17188,9 +13040,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -17199,38 +13052,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -17245,18 +13066,18 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "dev": true, "funding": [ { @@ -17274,7 +13095,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -17282,136 +13103,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -17448,9 +13139,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -17462,33 +13153,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -17498,20 +13162,6 @@ "node": ">=6" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -17543,9 +13193,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -17561,24 +13211,6 @@ "node": ">=6" } }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/qrcode.react": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", @@ -17589,12 +13221,12 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -17633,16 +13265,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -17653,34 +13275,18 @@ } }, "node_modules/raw-body": { - "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==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.7.0", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, "engines": { - "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": ">= 0.8" } }, "node_modules/rc": { @@ -17708,309 +13314,640 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.0.0" } }, "node_modules/react-easy-sort": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.8.0.tgz", - "integrity": "sha512-6CUvG0rPyO8H9MTel38r/gmPemIKcOSkvgZQtrxILYFPfGZnmkLVU3YSVHEg22D+pJMoeVRdJpuF2kD2dqeIEw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.6.0.tgz", + "integrity": "sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg==", "license": "MIT", "dependencies": { - "tslib": "^2.8.1" + "array-move": "^3.0.1", + "tslib": "2.0.1" }, "engines": { - "node": ">=18" + "node": ">=16" }, "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.3.0", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", - "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.6.tgz", + "integrity": "sha512-RzMDZCRd2JFFkGljhBWNWGH2ti4Qnhcx03nR1uPW1vNBptqDJx/fxSJqzCDYEEpTkWPaEe2unHM4CdzRAI7awg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/traverse": "^7.27.0", - "chokidar": "^4.0.3", - "commander": "^13.0.0", - "debounce": "^2.0.0", - "esbuild": "^0.25.0", - "glob": "^11.0.0", - "jiti": "2.4.2", - "log-symbols": "^7.0.0", - "mime-types": "^3.0.0", - "normalize-path": "^3.0.0", - "nypm": "0.6.0", - "ora": "^8.0.0", - "prompts": "2.4.2", - "socket.io": "^4.8.1", - "tsconfig-paths": "4.2.0" + "@babel/parser": "7.24.5", + "@babel/traverse": "7.25.6", + "chalk": "4.1.2", + "chokidar": "4.0.3", + "commander": "11.1.0", + "debounce": "2.0.0", + "esbuild": "0.25.0", + "glob": "10.3.4", + "log-symbols": "4.1.0", + "mime-types": "2.1.35", + "next": "15.2.4", + "normalize-path": "3.0.0", + "ora": "5.4.1", + "socket.io": "4.8.1" }, "bin": { - "email": "dist/index.js" + "email": "dist/cli/index.js" }, "engines": { "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==", + "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "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==", + "node_modules/react-email/node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "balanced-match": "^1.0.0" } }, - "node_modules/react-email/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "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", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "node_modules/react-email/node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "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", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" + "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, - "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==", + "node_modules/react-email/node_modules/glob": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", "dev": true, "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=14" + "node": ">=16 || 14 >=14.17" }, "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==", + "node_modules/react-email/node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=18" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/react-email/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/react-email/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": "MIT", + "license": "ISC" + }, + "node_modules/react-email/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": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/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/react-hook-form": { - "version": "7.65.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", - "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -18054,9 +13991,9 @@ "license": "MIT" }, "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -18122,16 +14059,6 @@ } } }, - "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", @@ -18147,9 +14074,9 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, "license": "MIT", "engines": { @@ -18174,12 +14101,6 @@ "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", @@ -18222,33 +14143,16 @@ "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==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "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", @@ -18302,31 +14206,15 @@ } }, "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18438,66 +14326,9 @@ "license": "MIT" }, "node_modules/scheduler": { - "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, + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "license": "MIT" }, "node_modules/selderee": { @@ -18513,9 +14344,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -18525,50 +14356,66 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, - "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", + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { - "randombytes": "^2.1.0" + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, "node_modules/set-function-length": { @@ -18624,16 +14471,16 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", - "devOptional": true, + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -18642,28 +14489,25 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@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" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -18687,6 +14531,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -18811,12 +14668,14 @@ "simple-concat": "^1.0.0" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" + "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/slash": { "version": "3.0.0", @@ -18898,40 +14757,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -18964,20 +14789,6 @@ } } }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -18996,50 +14807,6 @@ } } }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/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", @@ -19070,25 +14837,10 @@ "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", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "license": "MIT" }, "node_modules/stack-trace": { @@ -19100,52 +14852,21 @@ "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", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stop-iteration-iterator": { + "node_modules/streamsearch": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", "engines": { - "node": ">= 0.4" + "node": ">=10.0.0" } }, "node_modules/string_decoder": { @@ -19324,9 +15045,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -19391,19 +15112,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -19427,126 +15135,6 @@ } } }, - "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", @@ -19572,9 +15160,9 @@ } }, "node_modules/swagger-ui-dist": { - "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==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -19596,9 +15184,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", "license": "MIT", "funding": { "type": "github", @@ -19606,46 +15194,24 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", - "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", + "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "license": "MIT", "engines": { "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tar": { - "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.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" } }, "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -19654,12 +15220,6 @@ "tar-stream": "^2.1.4" } }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -19676,138 +15236,12 @@ "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", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -19839,9 +15273,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "license": "MIT", "engines": { "node": ">=18.12" @@ -19850,23 +15284,15 @@ "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", - "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.10.tgz", + "integrity": "sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", - "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", @@ -19874,9 +15300,6 @@ }, "bin": { "tsc-alias": "dist/bin/index.js" - }, - "engines": { - "node": ">=16.20.2" } }, "node_modules/tsc-alias/node_modules/chokidar": { @@ -19993,9 +15416,9 @@ } }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20012,24 +15435,6 @@ "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", @@ -20043,9 +15448,9 @@ } }, "node_modules/tw-animate-css": { - "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==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz", + "integrity": "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -20063,25 +15468,14 @@ "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", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { "node": ">= 0.6" @@ -20162,9 +15556,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -20174,30 +15568,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "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.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" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -20217,9 +15587,9 @@ } }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "devOptional": true, "license": "MIT" }, @@ -20232,71 +15602,6 @@ "node": ">= 0.8" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@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", @@ -20327,33 +15632,6 @@ } } }, - "node_modules/use-debounce": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", - "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/use-intl": { - "version": "4.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", - "@schummar/icu-type-parser": "1.21.5", - "intl-messageformat": "^10.5.14" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" - } - }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -20377,9 +15655,9 @@ } }, "node_modules/use-sync-external-store": { - "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==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -20391,17 +15669,26 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist-node/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { @@ -20426,20 +15713,6 @@ "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", @@ -20459,112 +15732,6 @@ "node": ">= 8" } }, - "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": { - "@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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -20646,16 +15813,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -20667,13 +15833,13 @@ } }, "node_modules/winston": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", - "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", + "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -20815,9 +15981,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -20833,9 +15999,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -20853,99 +16019,100 @@ } } }, - "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", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14.6" + "node": ">= 14" } }, "node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { - "cliui": "^9.0.1", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" + "yargs-parser": "^21.1.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/yargs/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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/yargs/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==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, "node_modules/yocto-queue": { @@ -20960,38 +16127,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors": { - "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": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.2.tgz", - "integrity": "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", "license": "MIT", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "zod": "^3.25.0" + "zod": "^3.18.0" } } } diff --git a/package.json b/package.json index 4530e169..f2ce2cd4 100644 --- a/package.json +++ b/package.json @@ -12,144 +12,123 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "scripts": { "dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts", - "db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts", - "db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts", - "db:pg:push": "npx tsx server/db/pg/migrate.ts", - "db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts", - "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", - "email": "email dev --dir server/emails/templates --port 3005", - "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs" + "db:generate": "drizzle-kit generate", + "db:push": "npx tsx server/db/migrate.ts", + "db:studio": "drizzle-kit studio", + "build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs", + "start": "NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", + "email": "email dev --dir server/emails/templates --port 3005" }, "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "5.2.2", - "@node-rs/argon2": "^2.0.2", + "@asteasolutions/zod-to-openapi": "^7.3.0", + "@hookform/resolvers": "3.9.1", + "@node-rs/argon2": "2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", "@radix-ui/react-icons": "1.3.2", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@react-email/components": "0.5.6", - "@react-email/render": "^1.3.2", - "@react-email/tailwind": "1.2.2", - "@simplewebauthn/browser": "^13.2.2", - "@simplewebauthn/server": "^13.2.2", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "^1.1.4", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@react-email/components": "0.0.36", + "@react-email/render": "^1.0.6", + "@react-email/tailwind": "1.0.4", "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-table": "8.21.3", - "arctic": "^3.7.0", - "axios": "^1.12.2", + "@tanstack/react-table": "8.20.6", + "arctic": "^3.6.0", + "axios": "1.8.4", "better-sqlite3": "11.7.0", "canvas-confetti": "1.9.3", - "class-variance-authority": "^0.7.1", + "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "cmdk": "1.1.1", + "cmdk": "1.0.4", "cookie": "^1.0.2", "cookie-parser": "1.4.7", "cookies": "^0.9.1", "cors": "2.8.5", "crypto-js": "^4.2.0", - "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", - "helmet": "8.1.0", + "drizzle-orm": "0.38.3", + "eslint": "9.17.0", + "eslint-config-next": "15.1.3", + "express": "4.21.2", + "express-rate-limit": "7.5.0", + "glob": "11.0.0", + "helmet": "8.0.0", "http-errors": "2.0.0", "i": "^0.3.7", - "input-otp": "1.4.2", + "input-otp": "1.4.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.545.0", - "maxmind": "5.0.0", + "lucide-react": "0.469.0", "moment": "2.30.1", - "next": "15.5.4", - "next-intl": "^4.3.12", - "next-themes": "0.4.6", + "next": "15.2.4", + "next-themes": "0.4.4", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "nodemailer": "7.0.9", - "npm": "^11.6.2", + "nodemailer": "6.9.16", + "npm": "^11.2.0", "oslo": "1.2.1", - "pg": "^8.16.2", "qrcode.react": "4.2.0", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-easy-sort": "^1.8.0", - "react-hook-form": "7.65.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-easy-sort": "^1.6.0", + "react-hook-form": "7.54.2", "react-icons": "^5.5.0", "rebuild": "0.1.2", - "resend": "^6.1.2", - "semver": "^7.7.3", + "semver": "7.6.3", "swagger-ui-express": "^5.0.1", - "tailwind-merge": "3.3.1", - "tw-animate-css": "^1.3.8", - "uuid": "^13.0.0", + "tailwind-merge": "2.6.0", + "tw-animate-css": "^1.2.5", + "uuid": "^11.1.0", "vaul": "1.1.2", - "winston": "3.18.3", + "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.3", - "yargs": "18.0.0", - "zod": "3.25.76", - "zod-validation-error": "3.5.2" + "ws": "8.18.0", + "zod": "3.24.1", + "zod-validation-error": "3.4.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.0", + "@dotenvx/dotenvx": "1.32.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "4.3.0", - "@tailwindcss/postcss": "^4.1.14", + "@tailwindcss/postcss": "^4.1.3", "@types/better-sqlite3": "7.6.12", - "@types/cookie-parser": "1.4.9", - "@types/cors": "2.8.19", + "@types/cookie-parser": "1.4.8", + "@types/cors": "2.8.17", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.3", - "@types/express-session": "^1.18.2", + "@types/express": "5.0.0", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "24.7.2", - "@types/nodemailer": "7.0.2", - "@types/pg": "8.15.5", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.1", - "@types/semver": "^7.7.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^22", + "@types/nodemailer": "6.4.17", + "@types/react": "19.1.1", + "@types/react-dom": "19.1.2", + "@types/semver": "7.5.8", "@types/swagger-ui-express": "^4.1.8", - "@types/ws": "8.18.1", + "@types/ws": "8.5.13", "@types/yargs": "17.0.33", - "drizzle-kit": "0.31.5", - "esbuild": "0.25.10", + "drizzle-kit": "0.30.6", + "esbuild": "0.25.2", "esbuild-node-externals": "1.18.0", "postcss": "^8", - "react-email": "4.3.0", + "react-email": "4.0.6", "tailwindcss": "^4.1.4", - "tsc-alias": "1.8.16", - "tsx": "4.20.6", + "tsc-alias": "1.8.10", + "tsx": "4.19.3", "typescript": "^5", - "typescript-eslint": "^8.46.0" + "yargs": "17.7.2" }, "overrides": { "emblor": { diff --git a/postcss.config.mjs b/postcss.config.mjs index 9d3299ad..8dde23ef 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,7 +1,7 @@ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { - "@tailwindcss/postcss": {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/public/auth-diagram1.png b/public/auth-diagram1.png deleted file mode 100644 index 92843a6d..00000000 Binary files a/public/auth-diagram1.png and /dev/null differ diff --git a/public/clip.gif b/public/clip.gif deleted file mode 100644 index 4202d679..00000000 Binary files a/public/clip.gif and /dev/null differ diff --git a/public/diagram-dark.svg b/public/diagram-dark.svg deleted file mode 100644 index 58e44f35..00000000 --- a/public/diagram-dark.svg +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/diagram.svg b/public/diagram.svg deleted file mode 100644 index 9e9e39fb..00000000 --- a/public/diagram.svg +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/idp/azure.png b/public/idp/azure.png deleted file mode 100644 index d6ec5baf..00000000 Binary files a/public/idp/azure.png and /dev/null differ diff --git a/public/idp/google.png b/public/idp/google.png deleted file mode 100644 index da097687..00000000 Binary files a/public/idp/google.png and /dev/null differ diff --git a/public/logo/pangolin_black.svg b/public/logo/pangolin_black.svg index 89f5a622..fd2b02ac 100644 --- a/public/logo/pangolin_black.svg +++ b/public/logo/pangolin_black.svg @@ -1,21 +1,22 @@ - - - - - - - - + showgrid="false" + inkscape:zoom="1.9583914" + inkscape:cx="209.86611" + inkscape:cy="262.20499" + inkscape:window-width="3840" + inkscape:window-height="2136" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg52" /> diff --git a/public/logo/pangolin_orange.svg b/public/logo/pangolin_orange.svg index 5e81a57f..a8823c9d 100644 --- a/public/logo/pangolin_orange.svg +++ b/public/logo/pangolin_orange.svg @@ -1,22 +1,39 @@ - - - - - - - + xmlns:svg="http://www.w3.org/2000/svg"> diff --git a/public/logo/pangolin_orange_192x192.png b/public/logo/pangolin_orange_192x192.png index 33fbf7b0..52e8659b 100644 Binary files a/public/logo/pangolin_orange_192x192.png and b/public/logo/pangolin_orange_192x192.png differ diff --git a/public/logo/pangolin_orange_512x512.png b/public/logo/pangolin_orange_512x512.png index ceed7e55..21f27644 100644 Binary files a/public/logo/pangolin_orange_512x512.png and b/public/logo/pangolin_orange_512x512.png differ diff --git a/public/logo/pangolin_orange_96x96.png b/public/logo/pangolin_orange_96x96.png index 76f23b9d..6d3821c2 100644 Binary files a/public/logo/pangolin_orange_96x96.png and b/public/logo/pangolin_orange_96x96.png differ diff --git a/public/logo/pangolin_profile_picture.png b/public/logo/pangolin_profile_picture.png deleted file mode 100644 index 20c5f72b..00000000 Binary files a/public/logo/pangolin_profile_picture.png and /dev/null differ diff --git a/public/logo/word_mark.png b/public/logo/word_mark.png index 27944d9c..d75a047c 100644 Binary files a/public/logo/word_mark.png and b/public/logo/word_mark.png differ diff --git a/public/logo/word_mark_black.png b/public/logo/word_mark_black.png deleted file mode 100644 index cc412165..00000000 Binary files a/public/logo/word_mark_black.png and /dev/null differ diff --git a/public/logo/word_mark_white.png b/public/logo/word_mark_white.png deleted file mode 100644 index cd02b58a..00000000 Binary files a/public/logo/word_mark_white.png and /dev/null differ diff --git a/public/screenshots/collage.png b/public/screenshots/collage.png new file mode 100644 index 00000000..c791e7ea Binary files /dev/null and b/public/screenshots/collage.png differ diff --git a/public/screenshots/create-api-key.png b/public/screenshots/create-api-key.png deleted file mode 100644 index ad0ef6a4..00000000 Binary files a/public/screenshots/create-api-key.png and /dev/null differ diff --git a/public/screenshots/create-idp.png b/public/screenshots/create-idp.png deleted file mode 100644 index e19ddec5..00000000 Binary files a/public/screenshots/create-idp.png and /dev/null differ diff --git a/public/screenshots/create-resource.png b/public/screenshots/create-resource.png deleted file mode 100644 index 3b21f22b..00000000 Binary files a/public/screenshots/create-resource.png and /dev/null differ diff --git a/public/screenshots/create-share-link.png b/public/screenshots/create-share-link.png deleted file mode 100644 index 18849501..00000000 Binary files a/public/screenshots/create-share-link.png and /dev/null differ diff --git a/public/screenshots/create-site.png b/public/screenshots/create-site.png deleted file mode 100644 index b5ff8048..00000000 Binary files a/public/screenshots/create-site.png and /dev/null differ diff --git a/public/screenshots/edit-resource.png b/public/screenshots/edit-resource.png deleted file mode 100644 index 2d21afa6..00000000 Binary files a/public/screenshots/edit-resource.png and /dev/null differ diff --git a/public/screenshots/hero.png b/public/screenshots/hero.png index 86216cf6..4e321ee1 100644 Binary files a/public/screenshots/hero.png and b/public/screenshots/hero.png differ diff --git a/public/screenshots/resource-auth.png b/public/screenshots/resource-auth.png deleted file mode 100644 index e9d39f4c..00000000 Binary files a/public/screenshots/resource-auth.png and /dev/null differ diff --git a/public/screenshots/resource-authentication.png b/public/screenshots/resource-authentication.png deleted file mode 100644 index 764cd616..00000000 Binary files a/public/screenshots/resource-authentication.png and /dev/null differ diff --git a/public/screenshots/resources.png b/public/screenshots/resources.png deleted file mode 100644 index 86216cf6..00000000 Binary files a/public/screenshots/resources.png and /dev/null differ diff --git a/public/screenshots/roles.png b/public/screenshots/roles.png deleted file mode 100644 index 09d27387..00000000 Binary files a/public/screenshots/roles.png and /dev/null differ diff --git a/public/screenshots/site-online.png b/public/screenshots/site-online.png deleted file mode 100644 index 0adef017..00000000 Binary files a/public/screenshots/site-online.png and /dev/null differ diff --git a/public/screenshots/sites-fade.png b/public/screenshots/sites-fade.png deleted file mode 100644 index 7e21c2cd..00000000 Binary files a/public/screenshots/sites-fade.png and /dev/null differ diff --git a/public/screenshots/sites.png b/public/screenshots/sites.png deleted file mode 100644 index 0aaa79d0..00000000 Binary files a/public/screenshots/sites.png and /dev/null differ diff --git a/public/screenshots/users.png b/public/screenshots/users.png deleted file mode 100644 index 91286e02..00000000 Binary files a/public/screenshots/users.png and /dev/null differ diff --git a/server/apiServer.ts b/server/apiServer.ts index 8f1c1600..824a860d 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -5,33 +5,27 @@ import config from "@server/lib/config"; import logger from "@server/logger"; import { errorHandlerMiddleware, - notFoundMiddleware + notFoundMiddleware, + rateLimitMiddleware } from "@server/middlewares"; import { authenticated, unauthenticated } from "@server/routers/external"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import helmet from "helmet"; -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 "@server/lib/rateLimitStore"; -import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; const dev = config.isDev; const externalPort = config.getRawConfig().server.external_port; export function createApiServer() { const apiServer = express(); - const prefix = `/api/v1`; - const trustProxy = config.getRawConfig().server.trust_proxy; - if (trustProxy) { - apiServer.set("trust proxy", trustProxy); + if (config.getRawConfig().server.trust_proxy) { + apiServer.set("trust proxy", 1); } const corsConfig = config.getRawConfig().server.cors; + const options = { ...(corsConfig?.origins ? { origin: corsConfig.origins } @@ -48,6 +42,7 @@ export function createApiServer() { }; logger.debug("Using CORS options", options); + apiServer.use(cors(options)); if (!dev) { @@ -55,35 +50,22 @@ export function createApiServer() { apiServer.use(csrfProtectionMiddleware); } - apiServer.use(stripDuplicateSesions); apiServer.use(cookieParser()); apiServer.use(express.json()); - // Add request timeout middleware - apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout - if (!dev) { apiServer.use( - rateLimit({ - windowMs: - config.getRawConfig().rate_limits.global.window_minutes * - 60 * - 1000, + rateLimitMiddleware({ + windowMin: + config.getRawConfig().rate_limits.global.window_minutes, max: config.getRawConfig().rate_limits.global.max_requests, - 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( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message) - ); - }, - store: createStore() + type: "IP_AND_PATH" }) ); } // API routes + const prefix = `/api/v1`; apiServer.use(logIncomingMiddleware); apiServer.use(prefix, unauthenticated); apiServer.use(prefix, authenticated); diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 8740c865..d974f03b 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -1,6 +1,6 @@ import { Request } from "express"; import { db } from "@server/db"; -import { userActions, roleActions, userOrgs } from "@server/db"; +import { userActions, roleActions, userOrgs } from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -57,11 +57,8 @@ export enum ActionsEnum { // removeUserAction = "removeUserAction", // removeUserSite = "removeUserSite", getOrgUser = "getOrgUser", - updateUser = "updateUser", - getUser = "getUser", setResourcePassword = "setResourcePassword", setResourcePincode = "setResourcePincode", - setResourceHeaderAuth = "setResourceHeaderAuth", setResourceWhitelist = "setResourceWhitelist", getResourceWhitelist = "getResourceWhitelist", generateAccessToken = "generateAccessToken", @@ -71,16 +68,6 @@ export enum ActionsEnum { deleteResourceRule = "deleteResourceRule", listResourceRules = "listResourceRules", updateResourceRule = "updateResourceRule", - createSiteResource = "createSiteResource", - deleteSiteResource = "deleteSiteResource", - getSiteResource = "getSiteResource", - listSiteResources = "listSiteResources", - updateSiteResource = "updateSiteResource", - createClient = "createClient", - deleteClient = "deleteClient", - updateClient = "updateClient", - listClients = "listClients", - getClient = "getClient", listOrgDomains = "listOrgDomains", createNewt = "createNewt", createIdp = "createIdp", @@ -99,25 +86,7 @@ export enum ActionsEnum { setApiKeyOrgs = "setApiKeyOrgs", 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" + getApiKey = "getApiKey" } export async function checkUserActionPermission( @@ -140,7 +109,7 @@ export async function checkUserActionPermission( try { let userRoleIds = req.userRoleIds; - // If userRoleIds is not available on the request, fetch it + // If userOrgRoleId is not available on the request, fetch it if (userRoleIds === undefined) { const userOrgRoles = await db .select({ roleId: userOrgs.roleId }) @@ -194,6 +163,7 @@ 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/canUserAccessResource.ts b/server/auth/canUserAccessResource.ts index a493148e..f322529c 100644 --- a/server/auth/canUserAccessResource.ts +++ b/server/auth/canUserAccessResource.ts @@ -1,6 +1,6 @@ -import { db } from "@server/db"; +import db from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; -import { roleResources, userResources } from "@server/db"; +import { roleResources, userResources } from "@server/db/schemas"; export async function canUserAccessResource({ userId, diff --git a/server/auth/checkValidInvite.ts b/server/auth/checkValidInvite.ts index e8dee8a8..bda12c9f 100644 --- a/server/auth/checkValidInvite.ts +++ b/server/auth/checkValidInvite.ts @@ -1,5 +1,5 @@ -import { db } from "@server/db"; -import { UserInvite, userInvites } from "@server/db"; +import db from "@server/db"; +import { UserInvite, userInvites } from "@server/db/schemas"; import { isWithinExpirationDate } from "oslo"; import { verifyPassword } from "./password"; import { eq } from "drizzle-orm"; diff --git a/server/auth/limits.ts b/server/auth/limits.ts new file mode 100644 index 00000000..c7c19398 --- /dev/null +++ b/server/auth/limits.ts @@ -0,0 +1,40 @@ +import { db } from '@server/db'; +import { limitsTable } from '@server/db/schemas'; +import { and, eq } from 'drizzle-orm'; +import createHttpError from 'http-errors'; +import HttpCode from '@server/types/HttpCode'; + +interface CheckLimitOptions { + orgId: string; + limitName: string; + currentValue: number; + increment?: number; +} + +export async function checkOrgLimit({ orgId, limitName, currentValue, increment = 0 }: CheckLimitOptions): Promise { + try { + const limit = await db.select() + .from(limitsTable) + .where( + and( + eq(limitsTable.orgId, orgId), + eq(limitsTable.name, limitName) + ) + ) + .limit(1); + + if (limit.length === 0) { + throw createHttpError(HttpCode.NOT_FOUND, `Limit "${limitName}" not found for organization`); + } + + const limitValue = limit[0].value; + + // Check if the current value plus the increment is within the limit + return (currentValue + increment) <= limitValue; + } catch (error) { + if (error instanceof Error) { + throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Error checking limit: ${error.message}`); + } + throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit'); + } +} diff --git a/server/auth/resourceOtp.ts b/server/auth/resourceOtp.ts index 3a0753e0..2539bf38 100644 --- a/server/auth/resourceOtp.ts +++ b/server/auth/resourceOtp.ts @@ -1,5 +1,5 @@ -import { db } from "@server/db"; -import { resourceOtp } from "@server/db"; +import db from "@server/db"; +import { resourceOtp } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import { createDate, isWithinExpirationDate, TimeSpan } from "oslo"; import { alphabet, generateRandomString, sha256 } from "oslo/crypto"; diff --git a/server/auth/sendEmailVerificationCode.ts b/server/auth/sendEmailVerificationCode.ts index 71112922..788c1358 100644 --- a/server/auth/sendEmailVerificationCode.ts +++ b/server/auth/sendEmailVerificationCode.ts @@ -1,7 +1,7 @@ import { TimeSpan, createDate } from "oslo"; import { generateRandomString, alphabet } from "oslo/crypto"; -import { db } from "@server/db"; -import { users, emailVerificationCodes } from "@server/db"; +import db from "@server/db"; +import { users, emailVerificationCodes } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { sendEmail } from "@server/emails"; import config from "@server/lib/config"; diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index e846396d..be43d7a8 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -3,8 +3,14 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { resourceSessions, Session, sessions, User, users } from "@server/db"; -import { db } from "@server/db"; +import { + resourceSessions, + Session, + sessions, + User, + users +} from "@server/db/schemas"; +import db from "@server/db"; import { eq, inArray } from "drizzle-orm"; import config from "@server/lib/config"; import type { RandomReader } from "@oslojs/crypto/random"; @@ -18,9 +24,8 @@ 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 = + "." + new URL(config.getRawConfig().app.dashboard_url).hostname; export function generateSessionToken(): string { const bytes = new Uint8Array(20); @@ -93,8 +98,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) { @@ -106,9 +111,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/newt.ts b/server/auth/sessions/newt.ts index 5e55c491..7d2ef8ab 100644 --- a/server/auth/sessions/newt.ts +++ b/server/auth/sessions/newt.ts @@ -2,8 +2,8 @@ import { encodeHexLowerCase, } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { Newt, newts, newtSessions, NewtSession } from "@server/db"; -import { db } from "@server/db"; +import { Newt, newts, newtSessions, NewtSession } from "@server/db/schemas"; +import db from "@server/db"; import { eq } from "drizzle-orm"; export const EXPIRES = 1000 * 60 * 60 * 24 * 30; diff --git a/server/auth/sessions/olm.ts b/server/auth/sessions/olm.ts deleted file mode 100644 index 89a0e81e..00000000 --- a/server/auth/sessions/olm.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - encodeHexLowerCase, -} from "@oslojs/encoding"; -import { sha256 } from "@oslojs/crypto/sha2"; -import { Olm, olms, olmSessions, OlmSession } from "@server/db"; -import { db } from "@server/db"; -import { eq } from "drizzle-orm"; - -export const EXPIRES = 1000 * 60 * 60 * 24 * 30; - -export async function createOlmSession( - token: string, - olmId: string, -): Promise { - const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), - ); - const session: OlmSession = { - sessionId: sessionId, - olmId, - expiresAt: new Date(Date.now() + EXPIRES).getTime(), - }; - await db.insert(olmSessions).values(session); - return session; -} - -export async function validateOlmSessionToken( - token: string, -): Promise { - const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), - ); - const result = await db - .select({ olm: olms, session: olmSessions }) - .from(olmSessions) - .innerJoin(olms, eq(olmSessions.olmId, olms.olmId)) - .where(eq(olmSessions.sessionId, sessionId)); - if (result.length < 1) { - return { session: null, olm: null }; - } - const { olm, session } = result[0]; - if (Date.now() >= session.expiresAt) { - await db - .delete(olmSessions) - .where(eq(olmSessions.sessionId, session.sessionId)); - return { session: null, olm: null }; - } - if (Date.now() >= session.expiresAt - (EXPIRES / 2)) { - session.expiresAt = new Date( - Date.now() + EXPIRES, - ).getTime(); - await db - .update(olmSessions) - .set({ - expiresAt: session.expiresAt, - }) - .where(eq(olmSessions.sessionId, session.sessionId)); - } - return { session, olm }; -} - -export async function invalidateOlmSession(sessionId: string): Promise { - await db.delete(olmSessions).where(eq(olmSessions.sessionId, sessionId)); -} - -export async function invalidateAllOlmSessions(olmId: string): Promise { - await db.delete(olmSessions).where(eq(olmSessions.olmId, olmId)); -} - -export type SessionValidationResult = - | { session: OlmSession; olm: Olm } - | { session: null; olm: null }; diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 31ab2b38..b95bece3 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -1,7 +1,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { resourceSessions, ResourceSession } from "@server/db"; -import { db } from "@server/db"; +import { resourceSessions, ResourceSession } from "@server/db/schemas"; +import db from "@server/db"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; @@ -173,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}`; } } @@ -190,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/auth/totp.ts b/server/auth/totp.ts index efe2b64b..3ca183a0 100644 --- a/server/auth/totp.ts +++ b/server/auth/totp.ts @@ -1,6 +1,6 @@ import { verify } from "@node-rs/argon2"; -import { db } from "@server/db"; -import { twoFactorBackupCodes } from "@server/db"; +import db from "@server/db"; +import { twoFactorBackupCodes } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { decodeHex } from "oslo/encoding"; import { TOTPController } from "oslo/otp"; diff --git a/server/auth/verifyResourceAccessToken.ts b/server/auth/verifyResourceAccessToken.ts index f1b587b7..8ddb5018 100644 --- a/server/auth/verifyResourceAccessToken.ts +++ b/server/auth/verifyResourceAccessToken.ts @@ -1,10 +1,10 @@ -import { db } from "@server/db"; +import db from "@server/db"; import { Resource, ResourceAccessToken, resourceAccessToken, resources -} from "@server/db"; +} from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import { isWithinExpirationDate } from "oslo"; import { verifyPassword } from "./password"; diff --git a/server/cleanup.ts b/server/cleanup.ts deleted file mode 100644 index de54ed77..00000000 --- a/server/cleanup.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/README.md b/server/db/README.md deleted file mode 100644 index 36c3730b..00000000 --- a/server/db/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Database - -Pangolin can use a Postgres or SQLite database to store its data. - -## Development - -### Postgres - -To use Postgres, edit `server/db/index.ts` to export all from `server/db/pg/index.ts`: - -```typescript -export * from "./pg"; -``` - -Make sure you have a valid config file with a connection string: - -```yaml -postgres: - connection_string: postgresql://postgres:postgres@localhost:5432 -``` - -You can run an ephemeral Postgres database for local development using Docker: - -```bash -docker run -d \ - --name postgres \ - --rm \ - -p 5432:5432 \ - -e POSTGRES_PASSWORD=postgres \ - -v $(mktemp -d):/var/lib/postgresql/data \ - postgres:17 -``` - -### Schema - -`server/db/pg/schema.ts` and `server/db/sqlite/schema.ts` contain the database schema definitions. These need to be kept in sync with with each other. - -Stick to common data types and avoid Postgres-specific features to ensure compatibility with SQLite. - -### SQLite - -To use SQLite, edit `server/db/index.ts` to export all from `server/db/sqlite/index.ts`: - -```typescript -export * from "./sqlite"; -``` - -No edits to the config are needed. If you keep the Postgres config, it will be ignored. - -## Generate and Push Migrations - -Ensure drizzle-kit is installed. - -### Postgres - -You must have a connection string in your config file, as shown above. - -```bash -npm run db:pg:generate -npm run db:pg:push -``` - -### SQLite - -```bash -npm run db:sqlite:generate -npm run db:sqlite:push -``` - -## Build Time - -There is a dockerfile for each database type. The dockerfile swaps out the `server/db/index.ts` file to use the correct database type. diff --git a/server/db/countries.ts b/server/db/countries.ts deleted file mode 100644 index 2907fd69..00000000 --- a/server/db/countries.ts +++ /dev/null @@ -1,1014 +0,0 @@ -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/sqlite/driver.ts b/server/db/index.ts similarity index 68% rename from server/db/sqlite/driver.ts rename to server/db/index.ts index 211ba8ea..6cf40fec 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/index.ts @@ -1,28 +1,24 @@ -import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; -import * as schema from "./schema/schema"; +import * as schema from "@server/db/schemas"; import path from "path"; -import fs from "fs"; +import fs from "fs/promises"; import { APP_PATH } from "@server/lib/consts"; import { existsSync, mkdirSync } from "fs"; export const location = path.join(APP_PATH, "db", "db.sqlite"); -export const exists = checkFileExists(location); +export const exists = await checkFileExists(location); bootstrapVolume(); -function createDb() { - const sqlite = new Database(location); - return DrizzleSqlite(sqlite, { schema }); -} +const sqlite = new Database(location); +export const db = drizzle(sqlite, { schema }); -export const db = createDb(); export default db; -export type Transaction = Parameters[0]>[0]; -function checkFileExists(filePath: string): boolean { +async function checkFileExists(filePath: string): Promise { try { - fs.accessSync(filePath); + await fs.access(filePath); return true; } catch { return false; diff --git a/server/db/maxmind.ts b/server/db/maxmind.ts deleted file mode 100644 index ca398df2..00000000 --- a/server/db/maxmind.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/sqlite/migrate.ts b/server/db/migrate.ts similarity index 89% rename from server/db/sqlite/migrate.ts rename to server/db/migrate.ts index e4a730d0..d39f4ae9 100644 --- a/server/db/sqlite/migrate.ts +++ b/server/db/migrate.ts @@ -1,5 +1,5 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { db } from "./driver"; +import db from "@server/db"; import path from "path"; const migrationsFolder = path.join("server/migrations"); @@ -7,7 +7,7 @@ const migrationsFolder = path.join("server/migrations"); const runMigrations = async () => { console.log("Running migrations..."); try { - migrate(db as any, { + migrate(db, { migrationsFolder: migrationsFolder, }); console.log("Migrations completed successfully."); diff --git a/server/db/names.ts b/server/db/names.ts index 2da38f10..21a37c9a 100644 --- a/server/db/names.ts +++ b/server/db/names.ts @@ -1,7 +1,7 @@ import { join } from "path"; import { readFileSync } from "fs"; -import { db, resources, siteResources } from "@server/db"; -import { exitNodes, sites } from "@server/db"; +import { db } from "@server/db"; +import { exitNodes, sites } from "./schemas/schema"; import { eq, and } from "drizzle-orm"; import { __DIRNAME } from "@server/lib/consts"; @@ -34,44 +34,6 @@ export async function getUniqueSiteName(orgId: string): Promise { } } -export async function getUniqueResourceName(orgId: string): Promise { - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - const name = generateName(); - const count = await db - .select({ niceId: resources.niceId, orgId: resources.orgId }) - .from(resources) - .where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))); - if (count.length === 0) { - return name; - } - loops++; - } -} - -export async function getUniqueSiteResourceName(orgId: string): Promise { - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - const name = generateName(); - const count = await db - .select({ niceId: siteResources.niceId, orgId: siteResources.orgId }) - .from(siteResources) - .where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId))); - if (count.length === 0) { - return name; - } - loops++; - } -} - export async function getUniqueExitNodeEndpointName(): Promise { let loops = 0; const count = await db @@ -97,7 +59,7 @@ export async function getUniqueExitNodeEndpointName(): Promise { export function generateName(): string { - const name = ( + return ( names.descriptors[ Math.floor(Math.random() * names.descriptors.length) ] + @@ -106,7 +68,4 @@ export function generateName(): string { ) .toLowerCase() .replace(/\s/g, "-"); - - // clean out any non-alphanumeric characters except for dashes - return name.replace(/[^a-z0-9-]/g, ""); } diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts deleted file mode 100644 index 6dbef7e8..00000000 --- a/server/db/pg/driver.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; -import { Pool } from "pg"; -import { readConfigFile } from "@server/lib/readConfigFile"; -import { withReplicas } from "drizzle-orm/pg-core"; - -function createDb() { - const config = readConfigFile(); - - if (!config.postgres) { - // 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; - const replicaConnections = config.postgres?.replicas || []; - - if (!connectionString) { - throw new Error( - "A primary db connection string is required in the configuration file." - ); - } - - // Create connection pools instead of individual connections - const poolConfig = config.postgres.pool; - const primaryPool = new Pool({ - connectionString, - max: poolConfig?.max_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, - }); - - const replicas = []; - - if (!replicaConnections.length) { - replicas.push(DrizzlePostgres(primaryPool)); - } else { - for (const conn of replicaConnections) { - const replicaPool = new Pool({ - connectionString: conn.connection_string, - max: poolConfig?.max_replica_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000, - }); - replicas.push(DrizzlePostgres(replicaPool)); - } - } - - return withReplicas(DrizzlePostgres(primaryPool), replicas as any); -} - -export const db = createDb(); -export default db; -export type Transaction = Parameters[0]>[0]; \ No newline at end of file diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts deleted file mode 100644 index 6e2c79f5..00000000 --- a/server/db/pg/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./driver"; -export * from "./schema/schema"; -export * from "./schema/privateSchema"; diff --git a/server/db/pg/migrate.ts b/server/db/pg/migrate.ts deleted file mode 100644 index 70b2ef54..00000000 --- a/server/db/pg/migrate.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { db } from "./driver"; -import path from "path"; - -const migrationsFolder = path.join("server/migrations"); - -const runMigrations = async () => { - console.log("Running migrations..."); - try { - await migrate(db as any, { - migrationsFolder: migrationsFolder - }); - console.log("Migrations completed successfully."); - } catch (error) { - console.error("Error running migrations:", error); - process.exit(1); - } -}; - -runMigrations(); diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts deleted file mode 100644 index 67fb28ec..00000000 --- a/server/db/pg/schema/privateSchema.ts +++ /dev/null @@ -1,232 +0,0 @@ -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/schema.ts b/server/db/pg/schema/schema.ts deleted file mode 100644 index 2e307c5f..00000000 --- a/server/db/pg/schema/schema.ts +++ /dev/null @@ -1,724 +0,0 @@ -import { - pgTable, - serial, - varchar, - boolean, - integer, - bigint, - real, - text -} from "drizzle-orm/pg-core"; -import { InferSelectModel } from "drizzle-orm"; -import { randomUUID } from "crypto"; - -export const domains = pgTable("domains", { - domainId: varchar("domainId").primaryKey(), - baseDomain: varchar("baseDomain").notNull(), - configManaged: boolean("configManaged").notNull().default(false), - type: varchar("type"), // "ns", "cname", "wildcard" - verified: boolean("verified").notNull().default(false), - failed: boolean("failed").notNull().default(false), - tries: integer("tries").notNull().default(0) -}); - -export const orgs = pgTable("orgs", { - orgId: varchar("orgId").primaryKey(), - name: varchar("name").notNull(), - subnet: varchar("subnet"), - createdAt: text("createdAt"), - settings: text("settings") // JSON blob of org-specific settings -}); - -export const orgDomains = pgTable("orgDomains", { - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - domainId: varchar("domainId") - .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) -}); - -export const sites = pgTable("sites", { - siteId: serial("siteId").primaryKey(), - orgId: varchar("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - niceId: varchar("niceId").notNull(), - exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, { - onDelete: "set null" - }), - name: varchar("name").notNull(), - pubKey: varchar("pubKey"), - subnet: varchar("subnet"), - megabytesIn: real("bytesIn").default(0), - megabytesOut: real("bytesOut").default(0), - lastBandwidthUpdate: varchar("lastBandwidthUpdate"), - type: varchar("type").notNull(), // "newt" or "wireguard" - online: boolean("online").notNull().default(false), - address: varchar("address"), - endpoint: varchar("endpoint"), - publicKey: varchar("publicKey"), - lastHolePunch: bigint("lastHolePunch", { mode: "number" }), - listenPort: integer("listenPort"), - dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true), - remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access -}); - -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" - }) - .notNull(), - niceId: text("niceId").notNull(), - name: varchar("name").notNull(), - subdomain: varchar("subdomain"), - fullDomain: varchar("fullDomain"), - domainId: varchar("domainId").references(() => domains.domainId, { - onDelete: "set null" - }), - ssl: boolean("ssl").notNull().default(false), - blockAccess: boolean("blockAccess").notNull().default(false), - sso: boolean("sso").notNull().default(true), - http: boolean("http").notNull().default(true), - protocol: varchar("protocol").notNull(), - proxyPort: integer("proxyPort"), - emailWhitelistEnabled: boolean("emailWhitelistEnabled") - .notNull() - .default(false), - applyRules: boolean("applyRules").notNull().default(false), - enabled: boolean("enabled").notNull().default(true), - stickySession: boolean("stickySession").notNull().default(false), - tlsServerName: varchar("tlsServerName"), - setHostHeader: varchar("setHostHeader"), - enableProxy: boolean("enableProxy").default(true), - skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { - onDelete: "cascade" - }), - headers: text("headers") // comma-separated list of headers to add to the request -}); - -export const targets = pgTable("targets", { - targetId: serial("targetId").primaryKey(), - resourceId: integer("resourceId") - .references(() => resources.resourceId, { - onDelete: "cascade" - }) - .notNull(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), - ip: varchar("ip").notNull(), - method: varchar("method"), - port: integer("port").notNull(), - internalPort: integer("internalPort"), - 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", { - exitNodeId: serial("exitNodeId").primaryKey(), - name: varchar("name").notNull(), - address: varchar("address").notNull(), - endpoint: varchar("endpoint").notNull(), - publicKey: varchar("publicKey").notNull(), - listenPort: integer("listenPort").notNull(), - reachableAt: varchar("reachableAt"), - maxConnections: integer("maxConnections"), - online: boolean("online").notNull().default(false), - lastPing: integer("lastPing"), - type: text("type").default("gerbil"), // gerbil, remoteExitNode - region: varchar("region") -}); - -export const siteResources = pgTable("siteResources", { - // this is for the clients - siteResourceId: serial("siteResourceId").primaryKey(), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - niceId: varchar("niceId").notNull(), - name: varchar("name").notNull(), - protocol: varchar("protocol").notNull(), - proxyPort: integer("proxyPort").notNull(), - destinationPort: integer("destinationPort").notNull(), - destinationIp: varchar("destinationIp").notNull(), - enabled: boolean("enabled").notNull().default(true) -}); - -export const users = pgTable("user", { - userId: varchar("id").primaryKey(), - email: varchar("email"), - username: varchar("username").notNull(), - name: varchar("name"), - type: varchar("type").notNull(), // "internal", "oidc" - idpId: integer("idpId").references(() => idp.idpId, { - onDelete: "cascade" - }), - passwordHash: varchar("passwordHash"), - twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false), - twoFactorSetupRequested: boolean("twoFactorSetupRequested").default(false), - twoFactorSecret: varchar("twoFactorSecret"), - emailVerified: boolean("emailVerified").notNull().default(false), - dateCreated: varchar("dateCreated").notNull(), - termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"), - termsVersion: varchar("termsVersion"), - serverAdmin: boolean("serverAdmin").notNull().default(false) -}); - -export const newts = pgTable("newt", { - newtId: varchar("id").primaryKey(), - secretHash: varchar("secretHash").notNull(), - dateCreated: varchar("dateCreated").notNull(), - version: varchar("version"), - siteId: integer("siteId").references(() => sites.siteId, { - onDelete: "cascade" - }) -}); - -export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", { - codeId: serial("id").primaryKey(), - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - codeHash: varchar("codeHash").notNull() -}); - -export const sessions = pgTable("session", { - sessionId: varchar("id").primaryKey(), - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const newtSessions = pgTable("newtSession", { - sessionId: varchar("id").primaryKey(), - newtId: varchar("newtId") - .notNull() - .references(() => newts.newtId, { onDelete: "cascade" }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const userOrgs = pgTable("userOrgs", { - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId), - isOwner: boolean("isOwner").notNull().default(false), - autoProvisioned: boolean("autoProvisioned").default(false) -}); - -export const emailVerificationCodes = pgTable("emailVerificationCodes", { - codeId: serial("id").primaryKey(), - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - email: varchar("email").notNull(), - code: varchar("code").notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const passwordResetTokens = pgTable("passwordResetTokens", { - tokenId: serial("id").primaryKey(), - email: varchar("email").notNull(), - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - tokenHash: varchar("tokenHash").notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const actions = pgTable("actions", { - actionId: varchar("actionId").primaryKey(), - name: varchar("name"), - description: varchar("description") -}); - -export const roles = pgTable("roles", { - roleId: serial("roleId").primaryKey(), - orgId: varchar("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - isAdmin: boolean("isAdmin"), - name: varchar("name").notNull(), - description: varchar("description") -}); - -export const roleActions = pgTable("roleActions", { - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), - actionId: varchar("actionId") - .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) -}); - -export const userActions = pgTable("userActions", { - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - actionId: varchar("actionId") - .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) -}); - -export const roleSites = pgTable("roleSites", { - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) -}); - -export const userSites = pgTable("userSites", { - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) -}); - -export const roleResources = pgTable("roleResources", { - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) -}); - -export const userResources = pgTable("userResources", { - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) -}); - -export const userInvites = pgTable("userInvites", { - inviteId: varchar("inviteId").primaryKey(), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - email: varchar("email").notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), - tokenHash: varchar("token").notNull(), - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }) -}); - -export const resourcePincode = pgTable("resourcePincode", { - pincodeId: serial("pincodeId").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - pincodeHash: varchar("pincodeHash").notNull(), - digitLength: integer("digitLength").notNull() -}); - -export const resourcePassword = pgTable("resourcePassword", { - passwordId: serial("passwordId").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - 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") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - tokenHash: varchar("tokenHash").notNull(), - sessionLength: bigint("sessionLength", { mode: "number" }).notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }), - title: varchar("title"), - description: varchar("description"), - createdAt: bigint("createdAt", { mode: "number" }).notNull() -}); - -export const resourceSessions = pgTable("resourceSessions", { - sessionId: varchar("id").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), - sessionLength: bigint("sessionLength", { mode: "number" }).notNull(), - doNotExtend: boolean("doNotExtend").notNull().default(false), - isRequestToken: boolean("isRequestToken"), - userSessionId: varchar("userSessionId").references( - () => sessions.sessionId, - { - onDelete: "cascade" - } - ), - passwordId: integer("passwordId").references( - () => resourcePassword.passwordId, - { - onDelete: "cascade" - } - ), - pincodeId: integer("pincodeId").references( - () => resourcePincode.pincodeId, - { - onDelete: "cascade" - } - ), - whitelistId: integer("whitelistId").references( - () => resourceWhitelist.whitelistId, - { - onDelete: "cascade" - } - ), - accessTokenId: varchar("accessTokenId").references( - () => resourceAccessToken.accessTokenId, - { - onDelete: "cascade" - } - ) -}); - -export const resourceWhitelist = pgTable("resourceWhitelist", { - whitelistId: serial("id").primaryKey(), - email: varchar("email").notNull(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) -}); - -export const resourceOtp = pgTable("resourceOtp", { - otpId: serial("otpId").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - email: varchar("email").notNull(), - otpHash: varchar("otpHash").notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const versionMigrations = pgTable("versionMigrations", { - version: varchar("version").primaryKey(), - executedAt: bigint("executedAt", { mode: "number" }).notNull() -}); - -export const resourceRules = pgTable("resourceRules", { - ruleId: serial("ruleId").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - enabled: boolean("enabled").notNull().default(true), - priority: integer("priority").notNull(), - action: varchar("action").notNull(), // ACCEPT, DROP, PASS - match: varchar("match").notNull(), // CIDR, PATH, IP - value: varchar("value").notNull() -}); - -export const supporterKey = pgTable("supporterKey", { - keyId: serial("keyId").primaryKey(), - key: varchar("key").notNull(), - githubUsername: varchar("githubUsername").notNull(), - phrase: varchar("phrase"), - tier: varchar("tier"), - valid: boolean("valid").notNull().default(false) -}); - -export const idp = pgTable("idp", { - idpId: serial("idpId").primaryKey(), - name: varchar("name").notNull(), - type: varchar("type").notNull(), - defaultRoleMapping: varchar("defaultRoleMapping"), - defaultOrgMapping: varchar("defaultOrgMapping"), - autoProvision: boolean("autoProvision").notNull().default(false) -}); - -export const idpOidcConfig = pgTable("idpOidcConfig", { - idpOauthConfigId: serial("idpOauthConfigId").primaryKey(), - idpId: integer("idpId") - .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), - variant: varchar("variant").notNull().default("oidc"), - clientId: varchar("clientId").notNull(), - clientSecret: varchar("clientSecret").notNull(), - authUrl: varchar("authUrl").notNull(), - tokenUrl: varchar("tokenUrl").notNull(), - identifierPath: varchar("identifierPath").notNull(), - emailPath: varchar("emailPath"), - namePath: varchar("namePath"), - scopes: varchar("scopes").notNull() -}); - -export const licenseKey = pgTable("licenseKey", { - licenseKeyId: varchar("licenseKeyId").primaryKey().notNull(), - instanceId: varchar("instanceId").notNull(), - token: varchar("token").notNull() -}); - -export const hostMeta = pgTable("hostMeta", { - hostMetaId: varchar("hostMetaId").primaryKey().notNull(), - createdAt: bigint("createdAt", { mode: "number" }).notNull() -}); - -export const apiKeys = pgTable("apiKeys", { - apiKeyId: varchar("apiKeyId").primaryKey(), - name: varchar("name").notNull(), - apiKeyHash: varchar("apiKeyHash").notNull(), - lastChars: varchar("lastChars").notNull(), - createdAt: varchar("dateCreated").notNull(), - isRoot: boolean("isRoot").notNull().default(false) -}); - -export const apiKeyActions = pgTable("apiKeyActions", { - apiKeyId: varchar("apiKeyId") - .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), - actionId: varchar("actionId") - .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }) -}); - -export const apiKeyOrg = pgTable("apiKeyOrg", { - apiKeyId: varchar("apiKeyId") - .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull() -}); - -export const idpOrg = pgTable("idpOrg", { - idpId: integer("idpId") - .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - roleMapping: varchar("roleMapping"), - orgMapping: varchar("orgMapping") -}); - -export const clients = pgTable("clients", { - clientId: serial("id").primaryKey(), - orgId: varchar("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, { - onDelete: "set null" - }), - name: varchar("name").notNull(), - pubKey: varchar("pubKey"), - subnet: varchar("subnet").notNull(), - megabytesIn: real("bytesIn"), - megabytesOut: real("bytesOut"), - lastBandwidthUpdate: varchar("lastBandwidthUpdate"), - lastPing: integer("lastPing"), - type: varchar("type").notNull(), // "olm" - online: boolean("online").notNull().default(false), - // endpoint: varchar("endpoint"), - lastHolePunch: integer("lastHolePunch"), - maxConnections: integer("maxConnections") -}); - -export const clientSites = pgTable("clientSites", { - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: boolean("isRelayed").notNull().default(false), - endpoint: varchar("endpoint") -}); - -export const olms = pgTable("olms", { - olmId: varchar("id").primaryKey(), - secretHash: varchar("secretHash").notNull(), - dateCreated: varchar("dateCreated").notNull(), - version: text("version"), - clientId: integer("clientId").references(() => clients.clientId, { - onDelete: "cascade" - }) -}); - -export const olmSessions = pgTable("clientSession", { - sessionId: varchar("id").primaryKey(), - olmId: varchar("olmId") - .notNull() - .references(() => olms.olmId, { onDelete: "cascade" }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() -}); - -export const userClients = pgTable("userClients", { - userId: varchar("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) -}); - -export const roleClients = pgTable("roleClients", { - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) -}); - -export const securityKeys = pgTable("webauthnCredentials", { - credentialId: varchar("credentialId").primaryKey(), - userId: varchar("userId") - .notNull() - .references(() => users.userId, { - onDelete: "cascade" - }), - publicKey: varchar("publicKey").notNull(), - signCount: integer("signCount").notNull(), - transports: varchar("transports"), - name: varchar("name"), - lastUsed: varchar("lastUsed").notNull(), - dateCreated: varchar("dateCreated").notNull(), - securityKeyName: varchar("securityKeyName") -}); - -export const webauthnChallenge = pgTable("webauthnChallenge", { - sessionId: varchar("sessionId").primaryKey(), - challenge: varchar("challenge").notNull(), - securityKeyName: varchar("securityKeyName"), - userId: varchar("userId").references(() => users.userId, { - onDelete: "cascade" - }), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp -}); - -export const setupTokens = pgTable("setupTokens", { - tokenId: varchar("tokenId").primaryKey(), - token: varchar("token").notNull(), - used: boolean("used").notNull().default(false), - dateCreated: varchar("dateCreated").notNull(), - dateUsed: varchar("dateUsed") -}); - -export type Org = InferSelectModel; -export type User = InferSelectModel; -export type Site = InferSelectModel; -export type Resource = InferSelectModel; -export type ExitNode = InferSelectModel; -export type Target = InferSelectModel; -export type Session = InferSelectModel; -export type Newt = InferSelectModel; -export type NewtSession = InferSelectModel; -export type EmailVerificationCode = InferSelectModel< - typeof emailVerificationCodes ->; -export type TwoFactorBackupCode = InferSelectModel; -export type PasswordResetToken = InferSelectModel; -export type Role = InferSelectModel; -export type Action = InferSelectModel; -export type RoleAction = InferSelectModel; -export type UserAction = InferSelectModel; -export type RoleSite = InferSelectModel; -export type UserSite = InferSelectModel; -export type RoleResource = InferSelectModel; -export type UserResource = 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; -export type VersionMigration = InferSelectModel; -export type ResourceRule = InferSelectModel; -export type Domain = InferSelectModel; -export type SupporterKey = InferSelectModel; -export type Idp = InferSelectModel; -export type ApiKey = InferSelectModel; -export type ApiKeyAction = InferSelectModel; -export type ApiKeyOrg = InferSelectModel; -export type Client = InferSelectModel; -export type ClientSite = InferSelectModel; -export type Olm = InferSelectModel; -export type OlmSession = InferSelectModel; -export type UserClient = InferSelectModel; -export type RoleClient = InferSelectModel; -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 deleted file mode 100644 index 9deaddca..00000000 --- a/server/db/queries/verifySessionQueries.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { db, loginPage, LoginPage, loginPageOrg } from "@server/db"; -import { - Resource, - ResourcePassword, - ResourcePincode, - ResourceRule, - resourcePassword, - resourcePincode, - resourceHeaderAuth, - ResourceHeaderAuth, - resourceRules, - resources, - roleResources, - sessions, - userOrgs, - userResources, - users -} from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; - -export type ResourceWithAuth = { - resource: Resource | null; - pincode: ResourcePincode | null; - password: ResourcePassword | null; - headerAuth: ResourceHeaderAuth | null; -}; - -export type UserSessionWithUser = { - session: any; - user: any; -}; - -/** - * Get resource by domain with pincode and password information - */ -export async function getResourceByDomain( - domain: string -): Promise { - const [result] = await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourceId, resources.resourceId) - ) - .where(eq(resources.fullDomain, domain)) - .limit(1); - - if (!result) { - return null; - } - - return { - resource: result.resources, - pincode: result.resourcePincode, - password: result.resourcePassword, - headerAuth: result.resourceHeaderAuth - }; -} - -/** - * Get user session with user information - */ -export async function getUserSessionWithUser( - userSessionId: string -): Promise { - const [res] = await db - .select() - .from(sessions) - .leftJoin(users, eq(users.userId, sessions.userId)) - .where(eq(sessions.sessionId, userSessionId)); - - if (!res) { - return null; - } - - return { - session: res.session, - user: res.user - }; -} - -/** - * Get user organization roles - */ -export async function getUserOrgRoles( - userId: string, - orgId: string -): Promise { - const userOrgRes = await db - .select() - .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))); - return userOrgRes.map((r) => r.roleId); -} - -/** - * Check if role has access to resource - */ -export async function getRoleResourceAccess( - resourceId: number, - roleIds: number[] -) { - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - inArray(roleResources.roleId, roleIds) - ) - ) - .limit(1); - - return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null; -} - -/** - * Check if user has direct access to resource - */ -export async function getUserResourceAccess( - userId: string, - resourceId: number -) { - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resourceId) - ) - ) - .limit(1); - - return userResourceAccess.length > 0 ? userResourceAccess[0] : null; -} - -/** - * Get resource rules for a given resource - */ -export async function getResourceRules( - resourceId: number -): Promise { - const rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); - - 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/schemas/hostMeta.ts b/server/db/schemas/hostMeta.ts new file mode 100644 index 00000000..e69de29b diff --git a/server/db/schemas/index.ts b/server/db/schemas/index.ts new file mode 100644 index 00000000..686fbd9e --- /dev/null +++ b/server/db/schemas/index.ts @@ -0,0 +1 @@ +export * from "./schema"; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/schemas/schema.ts similarity index 66% rename from server/db/sqlite/schema/schema.ts rename to server/db/schemas/schema.ts index 124a20be..7c790ebe 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/schemas/schema.ts @@ -1,4 +1,3 @@ -import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; @@ -7,28 +6,12 @@ export const domains = sqliteTable("domains", { baseDomain: text("baseDomain").notNull(), configManaged: integer("configManaged", { mode: "boolean" }) .notNull() - .default(false), - type: text("type"), // "ns", "cname", "wildcard" - verified: integer("verified", { mode: "boolean" }).notNull().default(false), - failed: integer("failed", { mode: "boolean" }).notNull().default(false), - tries: integer("tries").notNull().default(0) + .default(false) }); export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), - name: text("name").notNull(), - subnet: text("subnet"), - createdAt: text("createdAt"), - settings: text("settings") // JSON blob of org-specific settings -}); - -export const userDomains = sqliteTable("userDomains", { - userId: text("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - domainId: text("domainId") - .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) + name: text("name").notNull() }); export const orgDomains = sqliteTable("orgDomains", { @@ -53,37 +36,26 @@ export const sites = sqliteTable("sites", { }), name: text("name").notNull(), pubKey: text("pubKey"), - subnet: text("subnet"), - megabytesIn: integer("bytesIn").default(0), - megabytesOut: integer("bytesOut").default(0), + subnet: text("subnet").notNull(), + megabytesIn: integer("bytesIn"), + megabytesOut: integer("bytesOut"), lastBandwidthUpdate: text("lastBandwidthUpdate"), type: text("type").notNull(), // "newt" or "wireguard" - online: integer("online", { mode: "boolean" }).notNull().default(false), - - // exit node stuff that is how to connect to the site when it has a wg server - address: text("address"), // this is the address of the wireguard interface in newt - endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config - publicKey: text("publicKey"), // TODO: Fix typo in publicKey - lastHolePunch: integer("lastHolePunch"), - listenPort: integer("listenPort"), - dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) - .notNull() - .default(true), - remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access + online: integer("online", { mode: "boolean" }).notNull().default(false) }); export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - resourceGuid: text("resourceGuid", { length: 36 }) - .unique() - .notNull() - .$defaultFn(() => randomUUID()), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" }) .notNull(), - niceId: text("niceId").notNull(), name: text("name").notNull(), subdomain: text("subdomain"), fullDomain: text("fullDomain"), @@ -101,6 +73,7 @@ export const resources = sqliteTable("resources", { emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) .notNull() .default(false), + isBaseDomain: integer("isBaseDomain", { mode: "boolean" }), applyRules: integer("applyRules", { mode: "boolean" }) .notNull() .default(false), @@ -109,12 +82,7 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), tlsServerName: text("tlsServerName"), - setHostHeader: text("setHostHeader"), - enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), - skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { - onDelete: "cascade" - }), - headers: text("headers") // comma-separated list of headers to add to the request + setHostHeader: text("setHostHeader") }); export const targets = sqliteTable("targets", { @@ -124,42 +92,11 @@ export const targets = sqliteTable("targets", { onDelete: "cascade" }) .notNull(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), ip: text("ip").notNull(), method: text("method"), port: integer("port").notNull(), internalPort: integer("internalPort"), - 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" + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) }); export const exitNodes = sqliteTable("exitNodes", { @@ -169,32 +106,7 @@ export const exitNodes = sqliteTable("exitNodes", { endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config publicKey: text("publicKey").notNull(), listenPort: integer("listenPort").notNull(), - reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control - maxConnections: integer("maxConnections"), - online: integer("online", { mode: "boolean" }).notNull().default(false), - lastPing: integer("lastPing"), - type: text("type").default("gerbil"), // gerbil, remoteExitNode - region: text("region") -}); - -export const siteResources = sqliteTable("siteResources", { - // this is for the clients - siteResourceId: integer("siteResourceId").primaryKey({ - autoIncrement: true - }), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - niceId: text("niceId").notNull(), - name: text("name").notNull(), - protocol: text("protocol").notNull(), - proxyPort: integer("proxyPort").notNull(), - destinationPort: integer("destinationPort").notNull(), - destinationIp: text("destinationIp").notNull(), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + reachableAt: text("reachableAt") // this is the internal address of the gerbil http server for command control }); export const users = sqliteTable("user", { @@ -210,110 +122,25 @@ export const users = sqliteTable("user", { twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" }) .notNull() .default(false), - twoFactorSetupRequested: integer("twoFactorSetupRequested", { - mode: "boolean" - }).default(false), twoFactorSecret: text("twoFactorSecret"), emailVerified: integer("emailVerified", { mode: "boolean" }) .notNull() .default(false), dateCreated: text("dateCreated").notNull(), - termsAcceptedTimestamp: text("termsAcceptedTimestamp"), - termsVersion: text("termsVersion"), serverAdmin: integer("serverAdmin", { mode: "boolean" }) .notNull() .default(false) }); -export const securityKeys = sqliteTable("webauthnCredentials", { - credentialId: text("credentialId").primaryKey(), - userId: text("userId") - .notNull() - .references(() => users.userId, { - onDelete: "cascade" - }), - publicKey: text("publicKey").notNull(), - signCount: integer("signCount").notNull(), - transports: text("transports"), - name: text("name"), - lastUsed: text("lastUsed").notNull(), - dateCreated: text("dateCreated").notNull() -}); - -export const webauthnChallenge = sqliteTable("webauthnChallenge", { - sessionId: text("sessionId").primaryKey(), - challenge: text("challenge").notNull(), - securityKeyName: text("securityKeyName"), - userId: text("userId").references(() => users.userId, { - onDelete: "cascade" - }), - expiresAt: integer("expiresAt").notNull() // Unix timestamp -}); - -export const setupTokens = sqliteTable("setupTokens", { - tokenId: text("tokenId").primaryKey(), - token: text("token").notNull(), - used: integer("used", { mode: "boolean" }).notNull().default(false), - dateCreated: text("dateCreated").notNull(), - dateUsed: text("dateUsed") -}); - export const newts = sqliteTable("newt", { newtId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), - version: text("version"), siteId: integer("siteId").references(() => sites.siteId, { onDelete: "cascade" }) }); -export const clients = sqliteTable("clients", { - clientId: integer("id").primaryKey({ autoIncrement: true }), - orgId: text("orgId") - .references(() => orgs.orgId, { - onDelete: "cascade" - }) - .notNull(), - exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, { - onDelete: "set null" - }), - name: text("name").notNull(), - pubKey: text("pubKey"), - subnet: text("subnet").notNull(), - megabytesIn: integer("bytesIn"), - megabytesOut: integer("bytesOut"), - lastBandwidthUpdate: text("lastBandwidthUpdate"), - lastPing: integer("lastPing"), - type: text("type").notNull(), // "olm" - online: integer("online", { mode: "boolean" }).notNull().default(false), - // endpoint: text("endpoint"), - lastHolePunch: integer("lastHolePunch") -}); - -export const clientSites = sqliteTable("clientSites", { - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }), - siteId: integer("siteId") - .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: integer("isRelayed", { mode: "boolean" }) - .notNull() - .default(false), - endpoint: text("endpoint") -}); - -export const olms = sqliteTable("olms", { - olmId: text("id").primaryKey(), - secretHash: text("secretHash").notNull(), - dateCreated: text("dateCreated").notNull(), - version: text("version"), - clientId: integer("clientId").references(() => clients.clientId, { - onDelete: "cascade" - }) -}); - export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { codeId: integer("id").primaryKey({ autoIncrement: true }), userId: text("userId") @@ -338,14 +165,6 @@ export const newtSessions = sqliteTable("newtSession", { expiresAt: integer("expiresAt").notNull() }); -export const olmSessions = sqliteTable("clientSession", { - sessionId: text("id").primaryKey(), - olmId: text("olmId") - .notNull() - .references(() => olms.olmId, { onDelete: "cascade" }), - expiresAt: integer("expiresAt").notNull() -}); - export const userOrgs = sqliteTable("userOrgs", { userId: text("userId") .notNull() @@ -358,10 +177,7 @@ export const userOrgs = sqliteTable("userOrgs", { roleId: integer("roleId") .notNull() .references(() => roles.roleId), - isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), - autoProvisioned: integer("autoProvisioned", { - mode: "boolean" - }).default(false) + isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false) }); export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { @@ -444,24 +260,6 @@ export const userSites = sqliteTable("userSites", { .references(() => sites.siteId, { onDelete: "cascade" }) }); -export const userClients = sqliteTable("userClients", { - userId: text("userId") - .notNull() - .references(() => users.userId, { onDelete: "cascade" }), - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) -}); - -export const roleClients = sqliteTable("roleClients", { - roleId: integer("roleId") - .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), - clientId: integer("clientId") - .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) -}); - export const roleResources = sqliteTable("roleResources", { roleId: integer("roleId") .notNull() @@ -480,6 +278,18 @@ 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") @@ -514,16 +324,6 @@ 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") @@ -612,7 +412,7 @@ export const resourceRules = sqliteTable("resourceRules", { .references(() => resources.resourceId, { onDelete: "cascade" }), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), priority: integer("priority").notNull(), - action: text("action").notNull(), // ACCEPT, DROP, PASS + action: text("action").notNull(), // ACCEPT, DROP match: text("match").notNull(), // CIDR, PATH, IP value: text("value").notNull() }); @@ -636,7 +436,6 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { idpOauthConfigId: integer("idpOauthConfigId").primaryKey({ autoIncrement: true }), - variant: text("variant").notNull().default("oidc"), idpId: integer("idpId") .notNull() .references(() => idp.idpId, { onDelete: "cascade" }), @@ -704,8 +503,6 @@ export type Target = InferSelectModel; export type Session = InferSelectModel; export type Newt = InferSelectModel; export type NewtSession = InferSelectModel; -export type Olm = InferSelectModel; -export type OlmSession = InferSelectModel; export type EmailVerificationCode = InferSelectModel< typeof emailVerificationCodes >; @@ -719,30 +516,20 @@ 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; export type VersionMigration = InferSelectModel; export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; -export type Client = InferSelectModel; -export type ClientSite = InferSelectModel; -export type RoleClient = InferSelectModel; -export type UserClient = InferSelectModel; export type Idp = InferSelectModel; export type IdpOrg = InferSelectModel; export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; -export type SiteResource = InferSelectModel; -export type OrgDomains = InferSelectModel; -export type SetupToken = InferSelectModel; -export type HostMeta = InferSelectModel; -export type TargetHealthCheck = InferSelectModel; -export type IdpOidcConfig = InferSelectModel; diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts deleted file mode 100644 index 6e2c79f5..00000000 --- a/server/db/sqlite/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./driver"; -export * from "./schema/schema"; -export * from "./schema/privateSchema"; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts deleted file mode 100644 index 557ebfd6..00000000 --- a/server/db/sqlite/schema/privateSchema.ts +++ /dev/null @@ -1,226 +0,0 @@ -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/emails/index.ts b/server/emails/index.ts index 42cfa39c..46d1df69 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -18,10 +18,10 @@ function createEmailClient() { host: emailConfig.smtp_host, port: emailConfig.smtp_port, secure: emailConfig.smtp_secure || false, - auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? { + auth: { user: emailConfig.smtp_user, pass: emailConfig.smtp_pass - } : null + } } as SMTPTransport.Options; if (emailConfig.smtp_tls_reject_unauthorized !== undefined) { diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index c8a0b077..d7a59608 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -10,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"); @@ -24,16 +24,14 @@ export async function sendEmail( const emailHtml = await render(template); - 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 + name: opts.name || "Pangolin", + address: opts.from, }, to: opts.to, subject: opts.subject, - html: emailHtml + html: emailHtml, }); } diff --git a/server/emails/templates/NotifyResetPassword.tsx b/server/emails/templates/NotifyResetPassword.tsx index 66ea2430..aaa1cbdd 100644 --- a/server/emails/templates/NotifyResetPassword.tsx +++ b/server/emails/templates/NotifyResetPassword.tsx @@ -1,5 +1,11 @@ -import React from "react"; -import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { + Body, + Head, + Html, + Preview, + Tailwind +} from "@react-email/components"; +import * as React from "react"; import { themeColors } from "./lib/theme"; import { EmailContainer, @@ -16,29 +22,29 @@ interface Props { } export const ConfirmPasswordReset = ({ email }: Props) => { - const previewText = `Your password has been successfully reset.`; + const previewText = `Your password has been reset`; return ( {previewText} - + - {/* Password Successfully Reset */} + Password Reset Confirmation - Hi there, + Hi {email || "there"}, - Your password has been successfully reset. You can - now sign in to your account using your new password. + This email confirms that your password has just been + reset. If you made this change, no further action is + required. - If you didn't make this change, please contact our - support team immediately to secure your account. + Thank you for keeping your account secure. diff --git a/server/emails/templates/NotifyUsageLimitApproaching.tsx b/server/emails/templates/NotifyUsageLimitApproaching.tsx deleted file mode 100644 index beab0300..00000000 --- a/server/emails/templates/NotifyUsageLimitApproaching.tsx +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 783d1b0e..00000000 --- a/server/emails/templates/NotifyUsageLimitReached.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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/ResetPasswordCode.tsx b/server/emails/templates/ResetPasswordCode.tsx index df14b8be..1a79527b 100644 --- a/server/emails/templates/ResetPasswordCode.tsx +++ b/server/emails/templates/ResetPasswordCode.tsx @@ -1,5 +1,11 @@ -import React from "react"; -import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { + Body, + Head, + Html, + Preview, + Tailwind +} from "@react-email/components"; +import * as React from "react"; import { themeColors } from "./lib/theme"; import { EmailContainer, @@ -12,7 +18,6 @@ import { EmailText } from "./components/Email"; import CopyCodeBox from "./components/CopyCodeBox"; -import ButtonLink from "./components/ButtonLink"; interface Props { email: string; @@ -21,39 +26,37 @@ interface Props { } export const ResetPasswordCode = ({ email, code, link }: Props) => { - const previewText = `Reset your password with code: ${code}`; + const previewText = `Your password reset code is ${code}`; return ( {previewText} - + - {/* Reset Your Password */} + Password Reset Request - Hi there, + Hi {email || "there"}, - You've requested to reset your password. Click the - button below to reset your password, or use the - verification code provided if prompted. + You’ve requested to reset your password. Please{" "} + + click here + {" "} + and follow the instructions to reset your password, + or manually enter the following code: - - Reset Password - - - This reset code will expire in 2 hours. If you - didn't request a password reset, you can safely - ignore this email. + If you didn’t request this, you can safely ignore + this email. diff --git a/server/emails/templates/ResourceOTPCode.tsx b/server/emails/templates/ResourceOTPCode.tsx index 4f68d9df..086dc444 100644 --- a/server/emails/templates/ResourceOTPCode.tsx +++ b/server/emails/templates/ResourceOTPCode.tsx @@ -1,5 +1,11 @@ -import React from "react"; -import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { + Body, + Head, + Html, + Preview, + Tailwind +} from "@react-email/components"; +import * as React from "react"; import { EmailContainer, EmailLetterHead, @@ -26,40 +32,34 @@ export const ResourceOTPCode = ({ orgName: organizationName, otp }: ResourceOTPCodeProps) => { - const previewText = `Your access code for ${resourceName}: ${otp}`; + const previewText = `Your one-time password for ${resourceName} is ${otp}`; return ( {previewText} - + - {/* */} - {/* Access Code for {resourceName} */} - {/* */} + + Your One-Time Code for {resourceName} + - Hi there, + Hi {email || "there"}, - You've requested access to{" "} + You’ve requested a one-time password to access{" "} {resourceName} in{" "} - {organizationName}. Use the - verification code below to complete your - authentication. + {organizationName}. Use the code + below to complete your authentication: - - This code will expire in 15 minutes. If you didn't - request this code, please ignore this email. - - diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx index c859d3d7..ed3c7b53 100644 --- a/server/emails/templates/SendInviteLink.tsx +++ b/server/emails/templates/SendInviteLink.tsx @@ -1,5 +1,11 @@ -import React from "react"; -import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { + Body, + Head, + Html, + Preview, + Tailwind, +} from "@react-email/components"; +import * as React from "react"; import { themeColors } from "./lib/theme"; import { EmailContainer, @@ -35,44 +41,35 @@ export const SendInviteLink = ({ {previewText} - + - {/* */} - {/* You're Invited to Join {orgName} */} - {/* */} + Invited to Join {orgName} - Hi there, + Hi {email || "there"}, - You've been invited to join{" "} + You’ve been invited to join the organization{" "} {orgName} - {inviterName ? ` by ${inviterName}` : ""}. Click the - button below to accept your invitation and get - started. + {inviterName ? ` by ${inviterName}.` : "."} Please + access the link below to accept the invite. + + + + This invite will expire in{" "} + + {expiresInDays}{" "} + {expiresInDays === "1" ? "day" : "days"}. + - Accept Invitation + Accept Invite to {orgName} - {/* */} - {/* If you're having trouble clicking the button, copy */} - {/* and paste the URL below into your web browser: */} - {/*
*/} - {/* {inviteLink} */} - {/*
*/} - - - This invite expires in {expiresInDays}{" "} - {expiresInDays === "1" ? "day" : "days"}. If the - link has expired, please contact the owner of the - organization to request a new invitation. - - diff --git a/server/emails/templates/TwoFactorAuthNotification.tsx b/server/emails/templates/TwoFactorAuthNotification.tsx index 3261023e..8993a3bd 100644 --- a/server/emails/templates/TwoFactorAuthNotification.tsx +++ b/server/emails/templates/TwoFactorAuthNotification.tsx @@ -1,5 +1,11 @@ -import React from "react"; -import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import { + Body, + Head, + Html, + Preview, + Tailwind +} from "@react-email/components"; +import * as React from "react"; import { themeColors } from "./lib/theme"; import { EmailContainer, @@ -17,52 +23,44 @@ interface Props { } export const TwoFactorAuthNotification = ({ email, enabled }: Props) => { - const previewText = `Two-Factor Authentication ${enabled ? "enabled" : "disabled"} for your account`; + const previewText = `Two-Factor Authentication has been ${enabled ? "enabled" : "disabled"}`; return ( {previewText} - + - {/* */} - {/* Security Update: 2FA{" "} */} - {/* {enabled ? "Enabled" : "Disabled"} */} - {/* */} + + Two-Factor Authentication{" "} + {enabled ? "Enabled" : "Disabled"} + - Hi there, + Hi {email || "there"}, - Two-factor authentication has been successfully{" "} - {enabled ? "enabled" : "disabled"}{" "} - on your account. + This email confirms that Two-Factor Authentication + has been successfully{" "} + {enabled ? "enabled" : "disabled"} on your account. {enabled ? ( - <> - - Your account is now protected with an - additional layer of security. Keep your - authentication method safe and accessible. - - + + With Two-Factor Authentication enabled, your + account is now more secure. Please ensure you + keep your authentication method safe. + ) : ( - <> - - We recommend re-enabling two-factor - authentication to keep your account secure. - - + + With Two-Factor Authentication disabled, your + account may be less secure. We recommend + enabling it to protect your account. + )} - - If you didn't make this change, please contact our - support team immediately. - - diff --git a/server/emails/templates/VerifyEmailCode.tsx b/server/emails/templates/VerifyEmailCode.tsx index 6a361648..ad0ef053 100644 --- a/server/emails/templates/VerifyEmailCode.tsx +++ b/server/emails/templates/VerifyEmailCode.tsx @@ -1,5 +1,5 @@ -import React from "react"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; +import * as React from "react"; import { themeColors } from "./lib/theme"; import { EmailContainer, @@ -24,24 +24,25 @@ export const VerifyEmail = ({ verificationCode, verifyLink }: VerifyEmailProps) => { - const previewText = `Verify your email with code: ${verificationCode}`; + const previewText = `Your verification code is ${verificationCode}`; return ( {previewText} - + - {/* Verify Your Email Address */} + Please Verify Your Email - Hi there, + Hi {username || "there"}, - Welcome! To complete your account setup, please - verify your email address using the code below. + You’ve requested to verify your email. Please use + the code below to complete the verification process + upon logging in. @@ -49,8 +50,7 @@ export const VerifyEmail = ({ - This verification code will expire in 15 minutes. If - you didn't create an account, you can safely ignore + If you didn’t request this, you can safely ignore this email. diff --git a/server/emails/templates/WelcomeQuickStart.tsx b/server/emails/templates/WelcomeQuickStart.tsx deleted file mode 100644 index cd18f8b5..00000000 --- a/server/emails/templates/WelcomeQuickStart.tsx +++ /dev/null @@ -1,131 +0,0 @@ -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, - EmailSection, - EmailSignature, - EmailText, - EmailInfoSection -} from "./components/Email"; -import ButtonLink from "./components/ButtonLink"; -import CopyCodeBox from "./components/CopyCodeBox"; - -interface WelcomeQuickStartProps { - username?: string; - link: string; - fallbackLink: string; - resourceMethod: string; - resourceHostname: string; - resourcePort: string | number; - resourceUrl: string; - cliCommand: string; -} - -export const WelcomeQuickStart = ({ - username, - link, - fallbackLink, - resourceMethod, - resourceHostname, - resourcePort, - resourceUrl, - cliCommand -}: WelcomeQuickStartProps) => { - const previewText = "Welcome! Here's what to do next"; - - return ( - - - {previewText} - - - - - - Hi there, - - - Thank you for trying out Pangolin! We're excited to - have you on board. - - - - To continue to configure your site, resources, and - other features, complete your account setup to - access the full dashboard. - - - - - View Your Dashboard - - {/*

*/} - {/* If the button above doesn't work, you can also */} - {/* use this{" "} */} - {/* */} - {/* link */} - {/* */} - {/* . */} - {/*

*/} -
- - -
- Connect your site using Newt -
-
-
- - {cliCommand} - -
-

- To learn how to use Newt, including more - installation methods, visit the{" "} - - docs - - . -

-
-
- - - {resourceUrl} - - ) - } - ]} - /> - - - - -
- -
- - ); -}; - -export default WelcomeQuickStart; diff --git a/server/emails/templates/components/ButtonLink.tsx b/server/emails/templates/components/ButtonLink.tsx index 618fed15..e32e1810 100644 --- a/server/emails/templates/components/ButtonLink.tsx +++ b/server/emails/templates/components/ButtonLink.tsx @@ -12,11 +12,7 @@ export default function ButtonLink({ return ( {children} diff --git a/server/emails/templates/components/CopyCodeBox.tsx b/server/emails/templates/components/CopyCodeBox.tsx index 3e4d1d08..ef48b383 100644 --- a/server/emails/templates/components/CopyCodeBox.tsx +++ b/server/emails/templates/components/CopyCodeBox.tsx @@ -2,15 +2,10 @@ import React from "react"; export default function CopyCodeBox({ text }: { text: string }) { return ( -
-
- - {text} - -
-

- Copy and paste this code when prompted -

+
+ + {text} +
); } diff --git a/server/emails/templates/components/Email.tsx b/server/emails/templates/components/Email.tsx index 1ebd40e0..c73e4c85 100644 --- a/server/emails/templates/components/Email.tsx +++ b/server/emails/templates/components/Email.tsx @@ -1,26 +1,47 @@ +import { Container } from "@react-email/components"; import React from "react"; -import { Container, Img } from "@react-email/components"; // EmailContainer: Wraps the entire email layout export function EmailContainer({ children }: { children: React.ReactNode }) { return ( - + {children} ); } -// EmailLetterHead: For branding with logo on dark background +// EmailLetterHead: For branding or logo at the top export function EmailLetterHead() { return ( -
- Pangolin Logo +
+ + + + + +
+ Pangolin + + {new Date().getFullYear()} +
); } @@ -28,22 +49,14 @@ export function EmailLetterHead() { // EmailHeading: For the primary message or headline export function EmailHeading({ children }: { children: React.ReactNode }) { return ( -
-

- {children} -

-
+

+ {children} +

); } export function EmailGreeting({ children }: { children: React.ReactNode }) { - return ( -
-

- {children} -

-
- ); + return

{children}

; } // EmailText: For general text content @@ -55,13 +68,9 @@ export function EmailText({ className?: string; }) { return ( -
-

- {children} -

-
+

+ {children} +

); } @@ -73,74 +82,20 @@ export function EmailSection({ children: React.ReactNode; className?: string; }) { - return ( -
{children}
- ); + return
{children}
; } // EmailFooter: For closing or signature export function EmailFooter({ children }: { children: React.ReactNode }) { - return ( - <> - {false && ( -
- {children} -

- For any questions or support, please contact us at: -
- support@fossorial.io -

-

- © {new Date().getFullYear()} Fossorial, Inc. All - rights reserved. -

-
- )} - - ); + return
{children}
; } export function EmailSignature() { return ( -
-

- Best regards, -
- The Pangolin Team -

-
- ); -} - -// EmailInfoSection: For structured key-value info (like resource details) -export function EmailInfoSection({ - title, - items -}: { - title?: string; - items: { label: string; value: React.ReactNode }[]; -}) { - return ( -
- {title && ( -
- {title} -
- )} - - - {items.map((item, idx) => ( - - - - - ))} - -
- {item.label} - - {item.value} -
-
+

+ Best regards, +
+ Fossorial +

); } diff --git a/server/emails/templates/lib/theme.ts b/server/emails/templates/lib/theme.ts index a10ff77a..ada77fd2 100644 --- a/server/emails/templates/lib/theme.ts +++ b/server/emails/templates/lib/theme.ts @@ -1,5 +1,3 @@ -import React from "react"; - export const themeColors = { theme: { extend: { diff --git a/server/index.ts b/server/index.ts index f80d24eb..7dacae1f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,48 +1,22 @@ -#! /usr/bin/env node import "./extendZod.ts"; import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; -import { - ApiKey, - ApiKeyOrg, - RemoteExitNode, - Session, - User, - UserOrg -} from "@server/db"; -import { createIntegrationApiServer } from "./integrationApiServer"; -import config from "@server/lib/config"; -import { setHostMeta } from "@server/lib/hostMeta"; -import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js"; -import { initCleanup } from "@server/cleanup"; +import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; +// import { createIntegrationApiServer } from "./integrationApiServer"; async function startServers() { - await setHostMeta(); - - await config.initServer(); - await runSetupFunctions(); // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); - - let nextServer; - nextServer = await createNextServer(); - if (config.getRawConfig().traefik.file_mode) { - const monitor = new TraefikConfigManager(); - await monitor.start(); - } + const nextServer = await createNextServer(); let integrationServer; - if (config.getRawConfig().flags?.enable_integration_api) { - integrationServer = createIntegrationApiServer(); - } - - await initCleanup(); + // integrationServer = createIntegrationApiServer(); return { apiServer, @@ -58,13 +32,12 @@ declare global { interface Request { apiKey?: ApiKey; user?: User; - session: Session; + session?: Session; userOrg?: UserOrg; apiKeyOrg?: ApiKeyOrg; userRoleIds?: number[]; userOrgId?: string; userOrgIds?: string[]; - remoteExitNode?: RemoteExitNode; } } } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts deleted file mode 100644 index cbbea83f..00000000 --- a/server/integrationApiServer.ts +++ /dev/null @@ -1,115 +0,0 @@ -import express from "express"; -import cors from "cors"; -import cookieParser from "cookie-parser"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import { - errorHandlerMiddleware, - notFoundMiddleware, -} from "@server/middlewares"; -import { authenticated, unauthenticated } from "@server/routers/integration"; -import { logIncomingMiddleware } from "./middlewares/logIncoming"; -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; - -export function createIntegrationApiServer() { - const apiServer = express(); - - const trustProxy = config.getRawConfig().server.trust_proxy; - if (trustProxy) { - apiServer.set("trust proxy", trustProxy); - } - - apiServer.use(cors()); - - if (!dev) { - apiServer.use(helmet()); - } - - apiServer.use(cookieParser()); - apiServer.use(express.json()); - - apiServer.use( - "/v1/docs", - swaggerUi.serve, - swaggerUi.setup(getOpenApiDocumentation()) - ); - - // API routes - const prefix = `/v1`; - apiServer.use(logIncomingMiddleware); - apiServer.use(prefix, unauthenticated); - apiServer.use(prefix, authenticated); - - // Error handling - apiServer.use(notFoundMiddleware); - apiServer.use(errorHandlerMiddleware); - - // Create HTTP server - const httpServer = apiServer.listen(externalPort, (err?: any) => { - if (err) throw err; - logger.info( - `Integration API server is running on http://localhost:${externalPort}` - ); - }); - - return httpServer; -} - -function getOpenApiDocumentation() { - const bearerAuth = registry.registerComponent( - "securitySchemes", - "Bearer Auth", - { - type: "http", - scheme: "bearer" - } - ); - - for (const def of registry.definitions) { - if (def.type === "route") { - def.route.security = [ - { - [bearerAuth.name]: [] - } - ]; - } - } - - registry.registerPath({ - method: "get", - path: "/", - description: "Health check", - tags: [], - request: {}, - responses: {} - }); - - const generator = new OpenApiGeneratorV3(registry.definitions); - - const generated = generator.generateDocument({ - openapi: "3.0.0", - info: { - version: "v1", - title: "Pangolin Integration API" - }, - 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 0c7f885d..92da137f 100644 --- a/server/internalServer.ts +++ b/server/internalServer.ts @@ -8,8 +8,7 @@ import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares"; -import { internalRouter } from "@server/routers/internal"; -import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; +import internal from "@server/routers/internal"; const internalPort = config.getRawConfig().server.internal_port; @@ -18,12 +17,11 @@ 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, internalRouter); + internalServer.use(prefix, internal); internalServer.use(notFoundMiddleware); internalServer.use(errorHandlerMiddleware); diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts deleted file mode 100644 index 6fac099a..00000000 --- a/server/lib/blueprints/applyBlueprint.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { db, newts, Target } from "@server/db"; -import { Config, ConfigSchema } from "./types"; -import { ProxyResourcesResults, updateProxyResources } from "./proxyResources"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { resources, targets, sites } from "@server/db"; -import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; -import { addTargets as addProxyTargets } from "@server/routers/newt/targets"; -import { addTargets as addClientTargets } from "@server/routers/client/targets"; -import { - ClientResourcesResults, - updateClientResources -} from "./clientResources"; - -export async function applyBlueprint( - orgId: string, - configData: unknown, - siteId?: number -): Promise { - // Validate the input data - const validationResult = ConfigSchema.safeParse(configData); - if (!validationResult.success) { - throw new Error(fromError(validationResult.error).toString()); - } - - const config: Config = validationResult.data; - - try { - let proxyResourcesResults: ProxyResourcesResults = []; - let clientResourcesResults: ClientResourcesResults = []; - await db.transaction(async (trx) => { - proxyResourcesResults = await updateProxyResources( - orgId, - config, - trx, - siteId - ); - clientResourcesResults = await updateClientResources( - orgId, - config, - trx, - siteId - ); - }); - - logger.debug( - `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` - ); - - // We need to update the targets on the newts from the successfully updated information - for (const result of proxyResourcesResults) { - for (const target of result.targetsToUpdate) { - const [site] = await db - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, target.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) - ) - ) - .limit(1); - - if (site) { - logger.debug( - `Updating target ${target.targetId} on site ${site.sites.siteId}` - ); - - // see if you can find a matching target health check from the healthchecksToUpdate array - const matchingHealthcheck = - result.healthchecksToUpdate.find( - (hc) => hc.targetId === target.targetId - ); - - await addProxyTargets( - site.newt.newtId, - [target], - matchingHealthcheck ? [matchingHealthcheck] : [], - result.proxyResource.protocol, - result.proxyResource.proxyPort - ); - } - } - } - - logger.debug( - `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` - ); - - // We need to update the targets on the newts from the successfully updated information - for (const result of clientResourcesResults) { - const [site] = await db - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, result.resource.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) - ) - ) - .limit(1); - - if (site) { - logger.debug( - `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` - ); - - await addClientTargets( - site.newt.newtId, - result.resource.destinationIp, - result.resource.destinationPort, - result.resource.protocol, - result.resource.proxyPort - ); - } - } - } catch (error) { - logger.error(`Failed to update database from config: ${error}`); - throw error; - } -} - -// await updateDatabaseFromConfig("org_i21aifypnlyxur2", { -// resources: { -// "resource-nice-id": { -// name: "this is my resource", -// protocol: "http", -// "full-domain": "level1.test.example.com", -// "host-header": "example.com", -// "tls-server-name": "example.com", -// auth: { -// pincode: 123456, -// password: "sadfasdfadsf", -// "sso-enabled": true, -// "sso-roles": ["Member"], -// "sso-users": ["owen@fossorial.io"], -// "whitelist-users": ["owen@fossorial.io"] -// }, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8000, -// healthcheck: { -// port: 8000, -// hostname: "localhost" -// } -// }, -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8001 -// } -// ] -// }, -// "resource-nice-id2": { -// name: "http server", -// protocol: "tcp", -// "proxy-port": 3000, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// port: 3000, -// } -// ] -// } -// } -// }); diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts deleted file mode 100644 index f69e4854..00000000 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { sendToClient } from "@server/routers/ws"; -import { processContainerLabels } from "./parseDockerContainers"; -import { applyBlueprint } from "./applyBlueprint"; -import { db, sites } from "@server/db"; -import { eq } from "drizzle-orm"; -import logger from "@server/logger"; - -export async function applyNewtDockerBlueprint( - siteId: number, - newtId: string, - containers: any -) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!site) { - logger.warn("Site not found in applyNewtDockerBlueprint"); - return; - } - - // logger.debug(`Applying Docker blueprint to site: ${siteId}`); - // logger.debug(`Containers: ${JSON.stringify(containers, null, 2)}`); - - try { - const blueprint = processContainerLabels(containers); - - logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`); - - // Update the blueprint in the database - await applyBlueprint(site.orgId, blueprint, site.siteId); - } catch (error) { - logger.error(`Failed to update database from config: ${error}`); - await sendToClient(newtId, { - type: "newt/blueprint/results", - data: { - success: false, - message: `Failed to update database from config: ${error}` - } - }); - return; - } - - await sendToClient(newtId, { - type: "newt/blueprint/results", - data: { - success: true, - message: "Config updated successfully" - } - }); -} diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts deleted file mode 100644 index 59bbc346..00000000 --- a/server/lib/blueprints/clientResources.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - SiteResource, - siteResources, - Transaction, -} from "@server/db"; -import { sites } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { - Config, -} from "./types"; -import logger from "@server/logger"; - -export type ClientResourcesResults = { - resource: SiteResource; -}[]; - -export async function updateClientResources( - orgId: string, - config: Config, - trx: Transaction, - siteId?: number -): Promise { - const results: ClientResourcesResults = []; - - for (const [resourceNiceId, resourceData] of Object.entries( - config["client-resources"] - )) { - const [existingResource] = await trx - .select() - .from(siteResources) - .where( - and( - eq(siteResources.orgId, orgId), - eq(siteResources.niceId, resourceNiceId) - ) - ) - .limit(1); - - const resourceSiteId = resourceData.site; - let site; - - if (resourceSiteId) { - // Look up site by niceId - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where( - and( - eq(sites.niceId, resourceSiteId), - eq(sites.orgId, orgId) - ) - ) - .limit(1); - } else if (siteId) { - // Use the provided siteId directly, but verify it belongs to the org - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - } else { - throw new Error(`Target site is required`); - } - - if (!site) { - throw new Error( - `Site not found: ${resourceSiteId} in org ${orgId}` - ); - } - - if (existingResource) { - // Update existing resource - const [updatedResource] = await trx - .update(siteResources) - .set({ - name: resourceData.name || resourceNiceId, - siteId: site.siteId, - proxyPort: resourceData["proxy-port"]!, - destinationIp: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol - }) - .where( - eq( - siteResources.siteResourceId, - existingResource.siteResourceId - ) - ) - .returning(); - - results.push({ resource: updatedResource }); - } else { - // Create new resource - const [newResource] = await trx - .insert(siteResources) - .values({ - orgId: orgId, - siteId: site.siteId, - niceId: resourceNiceId, - name: resourceData.name || resourceNiceId, - proxyPort: resourceData["proxy-port"]!, - destinationIp: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol - }) - .returning(); - - logger.info( - `Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}` - ); - - results.push({ resource: newResource }); - } - } - - return results; -} diff --git a/server/lib/blueprints/parseDockerContainers.ts b/server/lib/blueprints/parseDockerContainers.ts deleted file mode 100644 index 1510e6e1..00000000 --- a/server/lib/blueprints/parseDockerContainers.ts +++ /dev/null @@ -1,301 +0,0 @@ -import logger from "@server/logger"; -import { setNestedProperty } from "./parseDotNotation"; - -export type DockerLabels = { - [key: string]: string; -}; - -export type ParsedObject = { - [key: string]: any; -}; - -type ContainerPort = { - privatePort: number; - publicPort: number; - type: string; - ip: string; -}; - -type Container = { - id: string; - name: string; - image: string; - state: string; - status: string; - ports: ContainerPort[] | null; - labels: DockerLabels; - created: number; - networks: { [key: string]: any }; - hostname: string; -}; - -type Target = { - hostname?: string; - port?: number; - method?: string; - enabled?: boolean; - [key: string]: any; -}; - -type ResourceConfig = { - [key: string]: any; - targets?: (Target | null)[]; -}; - -function getContainerPort(container: Container): number | null { - if (!container.ports || container.ports.length === 0) { - return null; - } - // Return the first port's privatePort - return container.ports[0].privatePort; - // return container.ports[0].publicPort; -} - -export function processContainerLabels(containers: Container[]): { - "proxy-resources": { [key: string]: ResourceConfig }; - "client-resources": { [key: string]: ResourceConfig }; -} { - const result = { - "proxy-resources": {} as { [key: string]: ResourceConfig }, - "client-resources": {} as { [key: string]: ResourceConfig } - }; - - // Process each container - containers.forEach((container) => { - if (container.state !== "running") { - return; - } - - const proxyResourceLabels: DockerLabels = {}; - const clientResourceLabels: DockerLabels = {}; - - // Filter and separate proxy-resources and client-resources labels - Object.entries(container.labels).forEach(([key, value]) => { - if (key.startsWith("pangolin.proxy-resources.")) { - // remove the pangolin.proxy- prefix to get "resources.xxx" - const strippedKey = key.replace("pangolin.proxy-", ""); - proxyResourceLabels[strippedKey] = value; - } else if (key.startsWith("pangolin.client-resources.")) { - // remove the pangolin.client- prefix to get "resources.xxx" - const strippedKey = key.replace("pangolin.client-", ""); - clientResourceLabels[strippedKey] = value; - } - }); - - // Process proxy resources - if (Object.keys(proxyResourceLabels).length > 0) { - processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]); - } - - // Process client resources - if (Object.keys(clientResourceLabels).length > 0) { - processResourceLabels(clientResourceLabels, container, result["client-resources"]); - } - }); - - return result; -} - -function processResourceLabels( - resourceLabels: DockerLabels, - container: Container, - targetResult: { [key: string]: ResourceConfig } -) { - // Parse the labels using the existing parseDockerLabels logic - const tempResult: ParsedObject = {}; - Object.entries(resourceLabels).forEach(([key, value]) => { - setNestedProperty(tempResult, key, value); - }); - - // Merge into target result - if (tempResult.resources) { - Object.entries(tempResult.resources).forEach( - ([resourceKey, resourceConfig]: [string, any]) => { - // Initialize resource if it doesn't exist - if (!targetResult[resourceKey]) { - targetResult[resourceKey] = {}; - } - - // Merge all properties except targets - Object.entries(resourceConfig).forEach( - ([propKey, propValue]) => { - if (propKey !== "targets") { - targetResult[resourceKey][propKey] = propValue; - } - } - ); - - // Handle targets specially - if ( - resourceConfig.targets && - Array.isArray(resourceConfig.targets) - ) { - const resource = targetResult[resourceKey]; - if (resource) { - if (!resource.targets) { - resource.targets = []; - } - - resourceConfig.targets.forEach( - (target: any, targetIndex: number) => { - // check if the target is an empty object - if ( - typeof target === "object" && - Object.keys(target).length === 0 - ) { - logger.debug( - `Skipping null target at index ${targetIndex} for resource ${resourceKey}` - ); - resource.targets!.push(null); - return; - } - - // Ensure targets array is long enough - while ( - resource.targets!.length <= targetIndex - ) { - resource.targets!.push({}); - } - - // Set default hostname and port if not provided - const finalTarget = { ...target }; - if (!finalTarget.hostname) { - finalTarget.hostname = - container.name || - container.hostname; - } - if (!finalTarget.port) { - const containerPort = - getContainerPort(container); - if (containerPort !== null) { - finalTarget.port = containerPort; - } - } - - // Merge with existing target data - resource.targets![targetIndex] = { - ...resource.targets![targetIndex], - ...finalTarget - }; - } - ); - } - } - } - ); - } -} - -// // Test example -// const testContainers: Container[] = [ -// { -// id: "57e056cb0e3a", -// name: "nginx1", -// image: "nginxdemos/hello", -// state: "running", -// status: "Up 4 days", -// ports: [ -// { -// privatePort: 80, -// publicPort: 8000, -// type: "tcp", -// ip: "0.0.0.0" -// } -// ], -// labels: { -// "resources.nginx.name": "nginx", -// "resources.nginx.full-domain": "nginx.example.com", -// "resources.nginx.protocol": "http", -// "resources.nginx.targets[0].enabled": "true" -// }, -// created: 1756942725, -// networks: { -// owen_default: { -// networkId: -// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" -// } -// }, -// hostname: "57e056cb0e3a" -// }, -// { -// id: "58e056cb0e3b", -// name: "nginx2", -// image: "nginxdemos/hello", -// state: "running", -// status: "Up 4 days", -// ports: [ -// { -// privatePort: 80, -// publicPort: 8001, -// type: "tcp", -// ip: "0.0.0.0" -// } -// ], -// labels: { -// "resources.nginx.name": "nginx", -// "resources.nginx.full-domain": "nginx.example.com", -// "resources.nginx.protocol": "http", -// "resources.nginx.targets[1].enabled": "true" -// }, -// created: 1756942726, -// networks: { -// owen_default: { -// networkId: -// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" -// } -// }, -// hostname: "58e056cb0e3b" -// }, -// { -// id: "59e056cb0e3c", -// name: "api-server", -// image: "my-api:latest", -// state: "running", -// status: "Up 2 days", -// ports: [ -// { -// privatePort: 3000, -// publicPort: 3000, -// type: "tcp", -// ip: "0.0.0.0" -// } -// ], -// labels: { -// "resources.api.name": "API Server", -// "resources.api.protocol": "http", -// "resources.api.targets[0].enabled": "true", -// "resources.api.targets[0].hostname": "custom-host", -// "resources.api.targets[0].port": "3001" -// }, -// created: 1756942727, -// networks: { -// owen_default: { -// networkId: -// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c" -// } -// }, -// hostname: "59e056cb0e3c" -// }, -// { -// id: "d0e29b08361c", -// name: "beautiful_wilson", -// image: "bolkedebruin/rdpgw:latest", -// state: "exited", -// status: "Exited (0) 4 hours ago", -// ports: null, -// labels: {}, -// created: 1757359039, -// networks: { -// bridge: { -// networkId: -// "ea7f56dfc9cc476b8a3560b5b570d0fe8a6a2bc5e8343ab1ed37822086e89687" -// } -// }, -// hostname: "d0e29b08361c" -// } -// ]; - -// // Test the function -// const result = processContainerLabels(testContainers); -// console.log("Processed result:"); -// console.log(JSON.stringify(result, null, 2)); diff --git a/server/lib/blueprints/parseDotNotation.ts b/server/lib/blueprints/parseDotNotation.ts deleted file mode 100644 index 87509d39..00000000 --- a/server/lib/blueprints/parseDotNotation.ts +++ /dev/null @@ -1,109 +0,0 @@ -export function setNestedProperty(obj: any, path: string, value: string): void { - const keys = path.split("."); - let current = obj; - - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - - // Handle array notation like "targets[0]" - const arrayMatch = key.match(/^(.+)\[(\d+)\]$/); - - if (arrayMatch) { - const [, arrayKey, indexStr] = arrayMatch; - const index = parseInt(indexStr, 10); - - // Initialize array if it doesn't exist - if (!current[arrayKey]) { - current[arrayKey] = []; - } - - // Ensure array is long enough - while (current[arrayKey].length <= index) { - current[arrayKey].push({}); - } - - current = current[arrayKey][index]; - } else { - // Regular object property - if (!current[key]) { - current[key] = {}; - } - current = current[key]; - } - } - - // Set the final value - const finalKey = keys[keys.length - 1]; - const arrayMatch = finalKey.match(/^(.+)\[(\d+)\]$/); - - if (arrayMatch) { - const [, arrayKey, indexStr] = arrayMatch; - const index = parseInt(indexStr, 10); - - if (!current[arrayKey]) { - current[arrayKey] = []; - } - - // Ensure array is long enough - while (current[arrayKey].length <= index) { - current[arrayKey].push(null); - } - - current[arrayKey][index] = convertValue(value); - } else { - current[finalKey] = convertValue(value); - } -} - -// Helper function to convert string values to appropriate types -export function convertValue(value: string): any { - // Convert boolean strings - if (value === "true") return true; - if (value === "false") return false; - - // Convert numeric strings - if (/^\d+$/.test(value)) { - const num = parseInt(value, 10); - return num; - } - - if (/^\d*\.\d+$/.test(value)) { - const num = parseFloat(value); - return num; - } - - // Return as string - return value; -} - -// // Example usage: -// const dockerLabels: DockerLabels = { -// "resources.resource-nice-id.name": "this is my resource", -// "resources.resource-nice-id.protocol": "http", -// "resources.resource-nice-id.full-domain": "level1.test3.example.com", -// "resources.resource-nice-id.host-header": "example.com", -// "resources.resource-nice-id.tls-server-name": "example.com", -// "resources.resource-nice-id.auth.pincode": "123456", -// "resources.resource-nice-id.auth.password": "sadfasdfadsf", -// "resources.resource-nice-id.auth.sso-enabled": "true", -// "resources.resource-nice-id.auth.sso-roles[0]": "Member", -// "resources.resource-nice-id.auth.sso-users[0]": "owen@fossorial.io", -// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@fossorial.io", -// "resources.resource-nice-id.targets[0].hostname": "localhost", -// "resources.resource-nice-id.targets[0].method": "http", -// "resources.resource-nice-id.targets[0].port": "8000", -// "resources.resource-nice-id.targets[0].healthcheck.port": "8000", -// "resources.resource-nice-id.targets[0].healthcheck.hostname": "localhost", -// "resources.resource-nice-id.targets[1].hostname": "localhost", -// "resources.resource-nice-id.targets[1].method": "http", -// "resources.resource-nice-id.targets[1].port": "8001", -// "resources.resource-nice-id2.name": "this is other resource", -// "resources.resource-nice-id2.protocol": "tcp", -// "resources.resource-nice-id2.proxy-port": "3000", -// "resources.resource-nice-id2.targets[0].hostname": "localhost", -// "resources.resource-nice-id2.targets[0].port": "3000" -// }; - -// // Parse the labels -// const parsed = parseDockerLabels(dockerLabels); -// console.log(JSON.stringify(parsed, null, 2)); diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts deleted file mode 100644 index cc60f04b..00000000 --- a/server/lib/blueprints/proxyResources.ts +++ /dev/null @@ -1,1060 +0,0 @@ -import { - domains, - orgDomains, - Resource, - resourceHeaderAuth, - resourcePincode, - resourceRules, - resourceWhitelist, - roleResources, - roles, - Target, - TargetHealthCheck, - targetHealthCheck, - Transaction, - userOrgs, - userResources, - users -} from "@server/db"; -import { resources, targets, sites } from "@server/db"; -import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; -import { - Config, - ConfigSchema, - isTargetsOnlyResource, - 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"; -import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; - -export type ProxyResourcesResults = { - proxyResource: Resource; - targetsToUpdate: Target[]; - healthchecksToUpdate: TargetHealthCheck[]; -}[]; - -export async function updateProxyResources( - orgId: string, - config: Config, - trx: Transaction, - siteId?: number -): Promise { - const results: ProxyResourcesResults = []; - - for (const [resourceNiceId, resourceData] of Object.entries( - config["proxy-resources"] - )) { - const targetsToUpdate: Target[] = []; - const healthchecksToUpdate: TargetHealthCheck[] = []; - let resource: Resource; - - async function createTarget( // reusable function to create a target - resourceId: number, - targetData: TargetData - ) { - const targetSiteId = targetData.site; - let site; - - if (targetSiteId) { - // Look up site by niceId - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where( - and( - eq(sites.niceId, targetSiteId), - eq(sites.orgId, orgId) - ) - ) - .limit(1); - } else if (siteId) { - // Use the provided siteId directly, but verify it belongs to the org - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where( - and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) - ) - .limit(1); - } else { - throw new Error(`Target site is required`); - } - - if (!site) { - throw new Error( - `Site not found: ${targetSiteId} in org ${orgId}` - ); - } - - let internalPortToCreate; - if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( - site.siteId!, - trx - ); - internalPortToCreate = internalPort; - } else { - internalPortToCreate = targetData["internal-port"]; - } - - // Create target - const [newTarget] = await trx - .insert(targets) - .values({ - resourceId: resourceId, - siteId: site.siteId, - ip: targetData.hostname, - method: targetData.method, - port: targetData.port, - enabled: targetData.enabled, - internalPort: internalPortToCreate, - path: targetData.path, - 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 - const [existingResource] = await trx - .select() - .from(resources) - .where( - and( - eq(resources.niceId, resourceNiceId), - eq(resources.orgId, orgId) - ) - ) - .limit(1); - - const http = resourceData.protocol == "http"; - const protocol = - resourceData.protocol == "http" ? "tcp" : resourceData.protocol; - const resourceEnabled = - resourceData.enabled == undefined || resourceData.enabled == null - ? true - : resourceData.enabled; - const resourceSsl = - resourceData.ssl == undefined || resourceData.ssl == null - ? true - : resourceData.ssl; - let headers = ""; - if (resourceData.headers) { - headers = JSON.stringify(resourceData.headers); - } - - if (existingResource) { - let domain; - if (http) { - domain = await getDomain( - existingResource.resourceId, - resourceData["full-domain"]!, - orgId, - trx - ); - } - - // check if the only key in the resource is targets, if so, skip the update - if (isTargetsOnlyResource(resourceData)) { - logger.debug( - `Skipping update for resource ${existingResource.resourceId} as only targets are provided` - ); - resource = existingResource; - } else { - // Update existing resource - [resource] = await trx - .update(resources) - .set({ - name: resourceData.name || "Unnamed Resource", - protocol: protocol || "tcp", - http: http, - proxyPort: http ? null : resourceData["proxy-port"], - fullDomain: http ? resourceData["full-domain"] : null, - subdomain: domain ? domain.subdomain : null, - domainId: domain ? domain.domainId : null, - enabled: resourceEnabled, - sso: resourceData.auth?.["sso-enabled"] || false, - ssl: resourceSsl, - setHostHeader: resourceData["host-header"] || null, - tlsServerName: resourceData["tls-server-name"] || null, - emailWhitelistEnabled: resourceData.auth?.[ - "whitelist-users" - ] - ? resourceData.auth["whitelist-users"].length > 0 - : false, - headers: headers || null, - applyRules: - resourceData.rules && resourceData.rules.length > 0 - }) - .where( - eq(resources.resourceId, existingResource.resourceId) - ) - .returning(); - - await trx - .delete(resourcePassword) - .where( - eq( - resourcePassword.resourceId, - existingResource.resourceId - ) - ); - if (resourceData.auth?.password) { - const passwordHash = await hashPassword( - resourceData.auth.password - ); - - await trx.insert(resourcePassword).values({ - resourceId: existingResource.resourceId, - passwordHash - }); - } - - await trx - .delete(resourcePincode) - .where( - eq( - resourcePincode.resourceId, - existingResource.resourceId - ) - ); - if (resourceData.auth?.pincode) { - const pincodeHash = await hashPassword( - resourceData.auth.pincode.toString() - ); - - await trx.insert(resourcePincode).values({ - resourceId: existingResource.resourceId, - pincodeHash, - digitLength: 6 - }); - } - - 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( - existingResource.resourceId, - ssoRoles, - orgId, - trx - ); - } - - if (resourceData.auth?.["sso-users"]) { - const ssoUsers = resourceData.auth?.["sso-users"]; - await syncUserResources( - existingResource.resourceId, - ssoUsers, - orgId, - trx - ); - } - - if (resourceData.auth?.["whitelist-users"]) { - const whitelistUsers = - resourceData.auth?.["whitelist-users"]; - await syncWhitelistUsers( - existingResource.resourceId, - whitelistUsers, - orgId, - trx - ); - } - } - - const existingResourceTargets = await trx - .select() - .from(targets) - .where(eq(targets.resourceId, existingResource.resourceId)) - .orderBy(asc(targets.targetId)); - - // Create new targets - for (const [index, targetData] of resourceData.targets.entries()) { - if ( - !targetData || - (typeof targetData === "object" && - Object.keys(targetData).length === 0) - ) { - // If targetData is null or an empty object, we can skip it - continue; - } - const existingTarget = existingResourceTargets[index]; - - if (existingTarget) { - const targetSiteId = targetData.site; - let site; - - if (targetSiteId) { - // Look up site by niceId - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where( - and( - eq(sites.niceId, targetSiteId), - eq(sites.orgId, orgId) - ) - ) - .limit(1); - } else if (siteId) { - // Use the provided siteId directly, but verify it belongs to the org - [site] = await trx - .select({ siteId: sites.siteId }) - .from(sites) - .where( - and( - eq(sites.siteId, siteId), - eq(sites.orgId, orgId) - ) - ) - .limit(1); - } else { - throw new Error(`Target site is required`); - } - - if (!site) { - throw new Error( - `Site not found: ${targetSiteId} in org ${orgId}` - ); - } - - // update this target - const [updatedTarget] = await trx - .update(targets) - .set({ - siteId: site.siteId, - ip: targetData.hostname, - method: http ? targetData.method : null, - port: targetData.port, - enabled: targetData.enabled, - path: targetData.path, - pathMatchType: targetData["path-match"], - rewritePath: targetData.rewritePath, - rewritePathType: targetData["rewrite-match"], - priority: targetData.priority - }) - .where(eq(targets.targetId, existingTarget.targetId)) - .returning(); - - if (checkIfTargetChanged(existingTarget, updatedTarget)) { - let internalPortToUpdate; - if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( - site.siteId!, - trx - ); - internalPortToUpdate = internalPort; - } else { - internalPortToUpdate = targetData["internal-port"]; - } - - const [finalUpdatedTarget] = await trx // this double is so we can check the whole target before and after - .update(targets) - .set({ - internalPort: internalPortToUpdate - }) - .where( - eq(targets.targetId, existingTarget.targetId) - ) - .returning(); - - 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); - } - } - - if (existingResourceTargets.length > resourceData.targets.length) { - const targetsToDelete = existingResourceTargets.slice( - resourceData.targets.length - ); - logger.debug( - `Targets to delete: ${JSON.stringify(targetsToDelete)}` - ); - for (const target of targetsToDelete) { - if (!target) { - continue; - } - if (siteId && target.siteId !== siteId) { - logger.debug( - `Skipping target ${target.targetId} for deletion. Site ID does not match filter.` - ); - continue; // only delete targets for the specified siteId - } - logger.debug(`Deleting target ${target.targetId}`); - await trx - .delete(targets) - .where(eq(targets.targetId, target.targetId)); - } - } - - const existingRules = await trx - .select() - .from(resourceRules) - .where( - eq(resourceRules.resourceId, existingResource.resourceId) - ) - .orderBy(resourceRules.priority); - - // Sync rules - for (const [index, rule] of resourceData.rules?.entries() || []) { - const existingRule = existingRules[index]; - if (existingRule) { - if ( - existingRule.action !== getRuleAction(rule.action) || - existingRule.match !== rule.match.toUpperCase() || - existingRule.value !== rule.value - ) { - validateRule(rule); - await trx - .update(resourceRules) - .set({ - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: rule.value - }) - .where( - eq(resourceRules.ruleId, existingRule.ruleId) - ); - } - } else { - validateRule(rule); - await trx.insert(resourceRules).values({ - resourceId: existingResource.resourceId, - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: rule.value, - priority: index + 1 // start priorities at 1 - }); - } - } - - if (existingRules.length > (resourceData.rules?.length || 0)) { - const rulesToDelete = existingRules.slice( - resourceData.rules?.length || 0 - ); - for (const rule of rulesToDelete) { - await trx - .delete(resourceRules) - .where(eq(resourceRules.ruleId, rule.ruleId)); - } - } - - logger.debug(`Updated resource ${existingResource.resourceId}`); - } else { - // create a brand new resource - let domain; - if (http) { - domain = await getDomain( - undefined, - resourceData["full-domain"]!, - orgId, - trx - ); - } - - // Create new resource - const [newResource] = await trx - .insert(resources) - .values({ - orgId, - niceId: resourceNiceId, - name: resourceData.name || "Unnamed Resource", - protocol: protocol || "tcp", - http: http, - proxyPort: http ? null : resourceData["proxy-port"], - fullDomain: http ? resourceData["full-domain"] : null, - subdomain: domain ? domain.subdomain : null, - domainId: domain ? domain.domainId : null, - enabled: resourceEnabled, - sso: resourceData.auth?.["sso-enabled"] || false, - setHostHeader: resourceData["host-header"] || null, - tlsServerName: resourceData["tls-server-name"] || null, - ssl: resourceSsl, - headers: headers || null, - applyRules: - resourceData.rules && resourceData.rules.length > 0 - }) - .returning(); - - if (resourceData.auth?.password) { - const passwordHash = await hashPassword( - resourceData.auth.password - ); - - await trx.insert(resourcePassword).values({ - resourceId: newResource.resourceId, - passwordHash - }); - } - - if (resourceData.auth?.pincode) { - const pincodeHash = await hashPassword( - resourceData.auth.pincode.toString() - ); - - await trx.insert(resourcePincode).values({ - resourceId: newResource.resourceId, - pincodeHash, - digitLength: 6 - }); - } - - 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 - .select() - .from(roles) - .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) - .limit(1); - - if (!adminRole) { - throw new Error(`Admin role not found`); - } - - await trx.insert(roleResources).values({ - roleId: adminRole.roleId, - resourceId: newResource.resourceId - }); - - if (resourceData.auth?.["sso-roles"]) { - const ssoRoles = resourceData.auth?.["sso-roles"]; - await syncRoleResources( - newResource.resourceId, - ssoRoles, - orgId, - trx - ); - } - - if (resourceData.auth?.["sso-users"]) { - const ssoUsers = resourceData.auth?.["sso-users"]; - await syncUserResources( - newResource.resourceId, - ssoUsers, - orgId, - trx - ); - } - - if (resourceData.auth?.["whitelist-users"]) { - const whitelistUsers = resourceData.auth?.["whitelist-users"]; - await syncWhitelistUsers( - newResource.resourceId, - whitelistUsers, - orgId, - trx - ); - } - - // Create new targets - for (const targetData of resourceData.targets) { - if (!targetData) { - // If targetData is null or an empty object, we can skip it - continue; - } - await createTarget(newResource.resourceId, targetData); - } - - for (const [index, rule] of resourceData.rules?.entries() || []) { - validateRule(rule); - await trx.insert(resourceRules).values({ - resourceId: newResource.resourceId, - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: rule.value, - priority: index + 1 // start priorities at 1 - }); - } - - logger.debug(`Created resource ${newResource.resourceId}`); - } - - results.push({ - proxyResource: resource, - targetsToUpdate, - healthchecksToUpdate - }); - } - - return results; -} - -function getRuleAction(input: string) { - let action = "DROP"; - if (input == "allow") { - action = "ACCEPT"; - } else if (input == "deny") { - action = "DROP"; - } else if (input == "pass") { - action = "PASS"; - } - return action; -} - -function validateRule(rule: any) { - if (rule.match === "cidr") { - if (!isValidCIDR(rule.value)) { - throw new Error(`Invalid CIDR provided: ${rule.value}`); - } - } else if (rule.match === "ip") { - if (!isValidIP(rule.value)) { - throw new Error(`Invalid IP provided: ${rule.value}`); - } - } else if (rule.match === "path") { - if (!isValidUrlGlobPattern(rule.value)) { - throw new Error(`Invalid URL glob pattern: ${rule.value}`); - } - } -} - -async function syncRoleResources( - resourceId: number, - ssoRoles: string[], - orgId: string, - trx: Transaction -) { - const existingRoleResources = await trx - .select() - .from(roleResources) - .where(eq(roleResources.resourceId, resourceId)); - - for (const roleName of ssoRoles) { - if (roleName === "Admin") { - continue; // never add admin access - } - - const [role] = await trx - .select() - .from(roles) - .where(and(eq(roles.name, roleName), eq(roles.orgId, orgId))) - .limit(1); - - if (!role) { - throw new Error(`Role not found: ${roleName} in org ${orgId}`); - } - - const existingRoleResource = existingRoleResources.find( - (rr) => rr.roleId === role.roleId - ); - - if (!existingRoleResource) { - await trx.insert(roleResources).values({ - roleId: role.roleId, - resourceId: resourceId - }); - } - } - - for (const existingRoleResource of existingRoleResources) { - const [role] = await trx - .select() - .from(roles) - .where(eq(roles.roleId, existingRoleResource.roleId)) - .limit(1); - - if (role.isAdmin) { - continue; // never remove admin access - } - - if (role && !ssoRoles.includes(role.name)) { - await trx - .delete(roleResources) - .where( - and( - eq(roleResources.roleId, existingRoleResource.roleId), - eq(roleResources.resourceId, resourceId) - ) - ); - } - } -} - -async function syncUserResources( - resourceId: number, - ssoUsers: string[], - orgId: string, - trx: Transaction -) { - const existingUserResources = await trx - .select() - .from(userResources) - .where(eq(userResources.resourceId, resourceId)); - - for (const email of ssoUsers) { - const [user] = await trx - .select() - .from(users) - .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (!user) { - throw new Error(`User not found: ${email} in org ${orgId}`); - } - - const existingUserResource = existingUserResources.find( - (rr) => rr.userId === user.user.userId - ); - - if (!existingUserResource) { - await trx.insert(userResources).values({ - userId: user.user.userId, - resourceId: resourceId - }); - } - } - - for (const existingUserResource of existingUserResources) { - const [user] = await trx - .select() - .from(users) - .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where( - and( - eq(users.userId, existingUserResource.userId), - eq(userOrgs.orgId, orgId) - ) - ) - .limit(1); - - if (user && user.user.email && !ssoUsers.includes(user.user.email)) { - await trx - .delete(userResources) - .where( - and( - eq(userResources.userId, existingUserResource.userId), - eq(userResources.resourceId, resourceId) - ) - ); - } - } -} - -async function syncWhitelistUsers( - resourceId: number, - whitelistUsers: string[], - orgId: string, - trx: Transaction -) { - const existingWhitelist = await trx - .select() - .from(resourceWhitelist) - .where(eq(resourceWhitelist.resourceId, resourceId)); - - for (const email of whitelistUsers) { - const [user] = await trx - .select() - .from(users) - .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (!user) { - throw new Error(`User not found: ${email} in org ${orgId}`); - } - - const existingWhitelistEntry = existingWhitelist.find( - (w) => w.email === email - ); - - if (!existingWhitelistEntry) { - await trx.insert(resourceWhitelist).values({ - email, - resourceId: resourceId - }); - } - } - - for (const existingWhitelistEntry of existingWhitelist) { - if (!whitelistUsers.includes(existingWhitelistEntry.email)) { - await trx - .delete(resourceWhitelist) - .where( - and( - eq(resourceWhitelist.resourceId, resourceId), - eq( - resourceWhitelist.email, - existingWhitelistEntry.email - ) - ) - ); - } - } -} - -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 -): boolean { - if (!existing && incoming) return true; - if (existing && !incoming) return true; - if (!existing || !incoming) return false; - - if (existing.ip !== incoming.ip) return true; - if (existing.port !== incoming.port) return true; - if (existing.siteId !== incoming.siteId) return true; - - return false; -} - -async function getDomain( - resourceId: number | undefined, - fullDomain: string, - orgId: string, - trx: Transaction -) { - const [fullDomainExists] = await trx - .select({ resourceId: resources.resourceId }) - .from(resources) - .where( - and( - eq(resources.fullDomain, fullDomain), - eq(resources.orgId, orgId), - resourceId - ? ne(resources.resourceId, resourceId) - : isNotNull(resources.resourceId) - ) - ) - .limit(1); - - if (fullDomainExists) { - throw new Error( - `Resource already exists: ${fullDomain} in org ${orgId}` - ); - } - - const domain = await getDomainId(orgId, fullDomain, trx); - - if (!domain) { - throw new Error( - `Domain not found for full-domain: ${fullDomain} in org ${orgId}` - ); - } - - await createCertificate(domain.domainId, fullDomain, trx); - - return domain; -} - -async function getDomainId( - orgId: string, - fullDomain: string, - trx: Transaction -): Promise<{ subdomain: string | null; domainId: string } | null> { - const possibleDomains = await trx - .select() - .from(domains) - .innerJoin(orgDomains, eq(domains.domainId, orgDomains.domainId)) - .where(and(eq(orgDomains.orgId, orgId), eq(domains.verified, true))) - .execute(); - - if (possibleDomains.length === 0) { - return null; - } - - const validDomains = possibleDomains.filter((domain) => { - if (domain.domains.type == "ns" || domain.domains.type == "wildcard") { - return ( - fullDomain === domain.domains.baseDomain || - fullDomain.endsWith(`.${domain.domains.baseDomain}`) - ); - } else if (domain.domains.type == "cname") { - return fullDomain === domain.domains.baseDomain; - } - }); - - if (validDomains.length === 0) { - return null; - } - - const domainSelection = validDomains[0].domains; - const baseDomain = domainSelection.baseDomain; - - // remove the base domain of the domain - let subdomain = null; - if (domainSelection.type == "ns") { - if (fullDomain != baseDomain) { - subdomain = fullDomain.replace(`.${baseDomain}`, ""); - } - } - - // Return the first valid domain - return { - subdomain: subdomain, - domainId: domainSelection.domainId - }; -} diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts deleted file mode 100644 index 02f83f9d..00000000 --- a/server/lib/blueprints/types.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { z } from "zod"; - -export const SiteSchema = z.object({ - name: z.string().min(1).max(100), - "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(), - method: z.enum(["http", "https", "h2c"]).optional(), - hostname: z.string(), - port: z.number().int().min(1).max(65535), - 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(), - 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; - -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()) - .optional() - .default([]) - .refine((roles) => !roles.includes("Admin"), { - message: "Admin role cannot be included in sso-roles" - }), - "sso-users": z.array(z.string().email()).optional().default([]), - "whitelist-users": z.array(z.string().email()).optional().default([]), -}); - -export const RuleSchema = z.object({ - action: z.enum(["allow", "deny", "pass"]), - match: z.enum(["cidr", "path", "ip", "country"]), - value: z.string() -}); - -export const HeaderSchema = z.object({ - name: z.string().min(1), - value: z.string().min(1) -}); - -// Schema for individual resource -export const ResourceSchema = z - .object({ - name: z.string().optional(), - protocol: z.enum(["http", "tcp", "udp"]).optional(), - ssl: z.boolean().optional(), - "full-domain": z.string().optional(), - "proxy-port": z.number().int().min(1).max(65535).optional(), - enabled: z.boolean().optional(), - targets: z.array(TargetSchema.nullable()).optional().default([]), - auth: AuthSchema.optional(), - "host-header": z.string().optional(), - "tls-server-name": z.string().optional(), - headers: z.array(HeaderSchema).optional(), - rules: z.array(RuleSchema).optional() - }) - .refine( - (resource) => { - if (isTargetsOnlyResource(resource)) { - return true; - } - - // Otherwise, require name and protocol for full resource definition - return ( - resource.name !== undefined && resource.protocol !== undefined - ); - }, - { - message: - "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum", - path: ["name", "protocol"] - } - ) - .refine( - (resource) => { - if (isTargetsOnlyResource(resource)) { - return true; - } - - // If protocol is http, all targets must have method field - if (resource.protocol === "http") { - return resource.targets.every( - (target) => target == null || target.method !== undefined - ); - } - // If protocol is tcp or udp, no target should have method field - if (resource.protocol === "tcp" || resource.protocol === "udp") { - return resource.targets.every( - (target) => target == null || target.method === undefined - ); - } - return true; - }, - (resource) => { - if (resource.protocol === "http") { - return { - message: - "When protocol is 'http', all targets must have a 'method' field", - path: ["targets"] - }; - } - return { - message: - "When protocol is 'tcp' or 'udp', targets must not have a 'method' field", - path: ["targets"] - }; - } - ) - .refine( - (resource) => { - if (isTargetsOnlyResource(resource)) { - return true; - } - - // If protocol is http, it must have a full-domain - if (resource.protocol === "http") { - return ( - resource["full-domain"] !== undefined && - resource["full-domain"].length > 0 - ); - } - return true; - }, - { - message: - "When protocol is 'http', a 'full-domain' must be provided", - path: ["full-domain"] - } - ) - .refine( - (resource) => { - if (isTargetsOnlyResource(resource)) { - return true; - } - - // If protocol is tcp or udp, it must have both proxy-port - if (resource.protocol === "tcp" || resource.protocol === "udp") { - return resource["proxy-port"] !== undefined; - } - return true; - }, - { - message: - "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided", - path: ["proxy-port", "exit-node"] - } - ) - .refine( - (resource) => { - // Skip validation for targets-only resources - if (isTargetsOnlyResource(resource)) { - return true; - } - - // If protocol is tcp or udp, it must not have auth - if (resource.protocol === "tcp" || resource.protocol === "udp") { - return resource.auth === undefined; - } - return true; - }, - { - message: - "When protocol is 'tcp' or 'udp', 'auth' must not be provided", - path: ["auth"] - } - ); - -export function isTargetsOnlyResource(resource: any): boolean { - return Object.keys(resource).length === 1 && resource.targets; -} - -export const ClientResourceSchema = z.object({ - name: z.string().min(2).max(100), - site: z.string().min(2).max(100).optional(), - protocol: z.enum(["tcp", "udp"]), - "proxy-port": z.number().min(1).max(65535), - "hostname": z.string().min(1).max(255), - "internal-port": z.number().min(1).max(65535), - enabled: z.boolean().optional().default(true) -}); - -// Schema for the entire configuration object -export const ConfigSchema = z - .object({ - "proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}), - "client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}), - sites: z.record(z.string(), SiteSchema).optional().default({}) - }) - .refine( - // Enforce the full-domain uniqueness across resources in the same stack - (config) => { - // Extract all full-domain values with their resource keys - const fullDomainMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const fullDomain = resource["full-domain"]; - if (fullDomain) { - // Only process if full-domain is defined - if (!fullDomainMap.has(fullDomain)) { - fullDomainMap.set(fullDomain, []); - } - fullDomainMap.get(fullDomain)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(fullDomainMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, - (config) => { - // Extract duplicates for error message - const fullDomainMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const fullDomain = resource["full-domain"]; - if (fullDomain) { - // Only process if full-domain is defined - if (!fullDomainMap.has(fullDomain)) { - fullDomainMap.set(fullDomain, []); - } - fullDomainMap.get(fullDomain)!.push(resourceKey); - } - } - ); - - const duplicates = Array.from(fullDomainMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([fullDomain, resourceKeys]) => - `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - return { - message: `Duplicate 'full-domain' values found: ${duplicates}`, - path: ["resources"] - }; - } - ) - .refine( - // Enforce proxy-port uniqueness within proxy-resources - (config) => { - const proxyPortMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(proxyPortMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, - (config) => { - // Extract duplicates for error message - const proxyPortMap = new Map(); - - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); - } - } - ); - - const duplicates = Array.from(proxyPortMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([proxyPort, resourceKeys]) => - `port ${proxyPort} used by proxy-resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - return { - message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`, - path: ["proxy-resources"] - }; - } - ) - .refine( - // Enforce proxy-port uniqueness within client-resources - (config) => { - const proxyPortMap = new Map(); - - Object.entries(config["client-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); - } - } - ); - - // Find duplicates - const duplicates = Array.from(proxyPortMap.entries()).filter( - ([_, resourceKeys]) => resourceKeys.length > 1 - ); - - return duplicates.length === 0; - }, - (config) => { - // Extract duplicates for error message - const proxyPortMap = new Map(); - - Object.entries(config["client-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); - } - proxyPortMap.get(proxyPort)!.push(resourceKey); - } - } - ); - - const duplicates = Array.from(proxyPortMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([proxyPort, resourceKeys]) => - `port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - return { - message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`, - path: ["client-resources"] - }; - } - ); - -// Type inference from the schema -export type Site = z.infer; -export type Target = z.infer; -export type Resource = z.infer; -export type Config = z.infer; diff --git a/server/lib/canUserAccessResource.ts b/server/lib/canUserAccessResource.ts index a493148e..f322529c 100644 --- a/server/lib/canUserAccessResource.ts +++ b/server/lib/canUserAccessResource.ts @@ -1,6 +1,6 @@ -import { db } from "@server/db"; +import db from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; -import { roleResources, userResources } from "@server/db"; +import { roleResources, userResources } from "@server/db/schemas"; export async function canUserAccessResource({ userId, diff --git a/server/lib/certificates.ts b/server/lib/certificates.ts deleted file mode 100644 index a6c51c96..00000000 --- a/server/lib/certificates.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 0aeb65c3..00000000 --- a/server/lib/colorsSchema.ts +++ /dev/null @@ -1,29 +0,0 @@ -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 6049fa85..1937d41c 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -1,7 +1,169 @@ +import fs from "fs"; +import yaml from "js-yaml"; import { z } from "zod"; -import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; -import { configSchema, readConfigFile } from "./readConfigFile"; import { fromError } from "zod-validation-error"; +import { + __DIRNAME, + APP_VERSION, + configFilePath1, + configFilePath2 +} from "@server/lib/consts"; +import { passwordSchema } from "@server/auth/passwordSchema"; +import stoi from "./stoi"; + +const portSchema = z.number().positive().gt(0).lte(65535); + +const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { + return process.env[envVar] ?? valFromYaml; +}; + +const configSchema = z.object({ + app: z.object({ + dashboard_url: z + .string() + .url() + .optional() + .pipe(z.string().url()) + .transform((url) => url.toLowerCase()), + log_level: z.enum(["debug", "info", "warn", "error"]), + save_logs: z.boolean(), + log_failed_attempts: z.boolean().optional() + }), + domains: z + .record( + z.string(), + z.object({ + base_domain: z + .string() + .nonempty("base_domain must not be empty") + .transform((url) => url.toLowerCase()), + cert_resolver: z.string().optional(), + prefer_wildcard_cert: z.boolean().optional() + }) + ) + .refine( + (domains) => { + const keys = Object.keys(domains); + + if (keys.length === 0) { + return false; + } + + return true; + }, + { + message: "At least one domain must be defined" + } + ), + server: z.object({ + integration_port: portSchema + .optional() + .transform(stoi) + .pipe(portSchema.optional()), + external_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_port: portSchema.optional().transform(stoi).pipe(portSchema), + next_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_hostname: z.string().transform((url) => url.toLowerCase()), + session_cookie_name: z.string(), + resource_access_token_param: z.string(), + resource_access_token_headers: z.object({ + id: z.string(), + token: z.string() + }), + resource_session_request_param: z.string(), + 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.boolean().optional().default(true), + secret: z + .string() + .optional() + .transform(getEnvOrYaml("SERVER_SECRET")) + .pipe(z.string().min(8)) + }), + traefik: z.object({ + http_entrypoint: z.string(), + https_entrypoint: z.string().optional(), + additional_middlewares: z.array(z.string()).optional() + }), + gerbil: z.object({ + start_port: portSchema.optional().transform(stoi).pipe(portSchema), + base_endpoint: z + .string() + .optional() + .pipe(z.string()) + .transform((url) => url.toLowerCase()), + use_subdomain: z.boolean(), + subnet_group: z.string(), + block_size: z.number().positive().gt(0), + site_block_size: z.number().positive().gt(0) + }), + rate_limits: z.object({ + global: z.object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }), + auth: z + .object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }) + .optional() + }), + email: z + .object({ + smtp_host: z.string().optional(), + smtp_port: portSchema.optional(), + smtp_user: z.string().optional(), + smtp_pass: z.string().optional(), + smtp_secure: z.boolean().optional(), + smtp_tls_reject_unauthorized: z.boolean().optional(), + no_reply: z.string().email().optional() + }) + .optional(), + users: z.object({ + server_admin: z.object({ + email: z + .string() + .email() + .optional() + .transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL")) + .pipe(z.string().email()) + .transform((v) => v.toLowerCase()), + password: passwordSchema + .optional() + .transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD")) + .pipe(passwordSchema) + }) + }), + flags: z + .object({ + require_email_verification: z.boolean().optional(), + disable_signup_without_invite: z.boolean().optional(), + disable_user_create_org: z.boolean().optional(), + allow_raw_resources: z.boolean().optional(), + allow_base_domain_resources: z.boolean().optional(), + allow_local_sites: z.boolean().optional() + }) + .optional() +}); export class Config { private rawConfig!: z.infer; @@ -9,89 +171,92 @@ export class Config { isDev: boolean = process.env.ENVIRONMENT !== "prod"; constructor() { - const environment = readConfigFile(); + this.loadConfig(); + } - const { - data: parsedConfig, - success, - error - } = configSchema.safeParse(environment); + public loadConfig() { + const loadConfig = (configPath: string) => { + try { + const yamlContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(yamlContent); + return config; + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Error loading configuration file: ${error.message}` + ); + } + throw error; + } + }; - if (!success) { - const errors = fromError(error); - throw new Error(`Invalid configuration file: ${errors}`); + let environment: any; + if (fs.existsSync(configFilePath1)) { + environment = loadConfig(configFilePath1); + } else if (fs.existsSync(configFilePath2)) { + environment = loadConfig(configFilePath2); } - if ( - // @ts-ignore - parsedConfig.users || - process.env.USERS_SERVERADMIN_EMAIL || - process.env.USERS_SERVERADMIN_PASSWORD - ) { + if (process.env.APP_BASE_DOMAIN) { console.log( - "WARNING: Your admin credentials are still in the config file or environment variables. This method of setting admin credentials is no longer supported. It is recommended to remove them." + "You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/" ); } + if (!environment) { + throw new Error( + "No configuration file found. Please create one. https://docs.fossorial.io/" + ); + } + + const parsedConfig = configSchema.safeParse(environment); + + if (!parsedConfig.success) { + const errors = fromError(parsedConfig.error); + throw new Error(`Invalid configuration file: ${errors}`); + } + process.env.APP_VERSION = APP_VERSION; - process.env.NEXT_PORT = parsedConfig.server.next_port.toString(); + process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); process.env.SERVER_EXTERNAL_PORT = - parsedConfig.server.external_port.toString(); + parsedConfig.data.server.external_port.toString(); process.env.SERVER_INTERNAL_PORT = - parsedConfig.server.internal_port.toString(); - process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.flags + parsedConfig.data.server.internal_port.toString(); + process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags ?.require_email_verification ? "true" : "false"; - process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.flags + process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags ?.allow_raw_resources ? "true" : "false"; process.env.SESSION_COOKIE_NAME = - parsedConfig.server.session_cookie_name; - process.env.EMAIL_ENABLED = parsedConfig.email ? "true" : "false"; - process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.flags + parsedConfig.data.server.session_cookie_name; + process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false"; + process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags ?.disable_signup_without_invite ? "true" : "false"; - process.env.DISABLE_USER_CREATE_ORG = parsedConfig.flags + process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags ?.disable_user_create_org ? "true" : "false"; process.env.RESOURCE_ACCESS_TOKEN_PARAM = - parsedConfig.server.resource_access_token_param; + parsedConfig.data.server.resource_access_token_param; process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID = - parsedConfig.server.resource_access_token_headers.id; + parsedConfig.data.server.resource_access_token_headers.id; process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN = - parsedConfig.server.resource_access_token_headers.token; + parsedConfig.data.server.resource_access_token_headers.token; process.env.RESOURCE_SESSION_REQUEST_PARAM = - parsedConfig.server.resource_session_request_param; - process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url; - process.env.FLAGS_DISABLE_LOCAL_SITES = parsedConfig.flags - ?.disable_local_sites - ? "true" - : "false"; - process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES = parsedConfig.flags - ?.disable_basic_wireguard_sites + parsedConfig.data.server.resource_session_request_param; + process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags + ?.allow_base_domain_resources ? "true" : "false"; + process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; - process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients - ? "true" - : "false"; - - if (parsedConfig.server.maxmind_db_path) { - process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; - } - - this.rawConfig = parsedConfig; - } - - public async initServer() { - if (!this.rawConfig) { - throw new Error("Config not loaded. Call load() first."); - } + this.rawConfig = parsedConfig.data; } public getRawConfig() { @@ -105,9 +270,6 @@ export class Config { } public getDomain(domainId: string) { - if (!this.rawConfig.domains || !this.rawConfig.domains[domainId]) { - return null; - } return this.rawConfig.domains[domainId]; } } diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 8ad98167..94d2716e 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.11.0"; +export const APP_VERSION = "1.3.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); @@ -11,5 +11,3 @@ 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 deleted file mode 100644 index 43b26264..00000000 --- a/server/lib/corsWithLoginPage.ts +++ /dev/null @@ -1,92 +0,0 @@ -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 deleted file mode 100644 index 08e1cb0d..00000000 --- a/server/lib/createUserAccountOrg.ts +++ /dev/null @@ -1,181 +0,0 @@ -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/domainUtils.ts b/server/lib/domainUtils.ts deleted file mode 100644 index d043ca51..00000000 --- a/server/lib/domainUtils.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { db } from "@server/db"; -import { domains, orgDomains } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { subdomainSchema } from "@server/lib/schemas"; -import { fromError } from "zod-validation-error"; - -export type DomainValidationResult = { - success: true; - fullDomain: string; - subdomain: string | null; -} | { - success: false; - error: string; -}; - -/** - * Validates a domain and constructs the full domain based on domain type and subdomain. - * - * @param domainId - The ID of the domain to validate - * @param orgId - The organization ID to check domain access - * @param subdomain - Optional subdomain to append (for ns and wildcard domains) - * @returns DomainValidationResult with success status and either fullDomain/subdomain or error message - */ -export async function validateAndConstructDomain( - domainId: string, - orgId: string, - subdomain?: string | null -): Promise { - try { - // Query domain with organization access check - const [domainRes] = await db - .select() - .from(domains) - .where(eq(domains.domainId, domainId)) - .leftJoin( - orgDomains, - and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) - ); - - // Check if domain exists - if (!domainRes || !domainRes.domains) { - return { - success: false, - error: `Domain with ID ${domainId} not found` - }; - } - - // Check if organization has access to domain - if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) { - return { - success: false, - error: `Organization does not have access to domain with ID ${domainId}` - }; - } - - // Check if domain is verified - if (!domainRes.domains.verified) { - return { - success: false, - error: `Domain with ID ${domainId} is not verified` - }; - } - - // Construct full domain based on domain type - let fullDomain = ""; - let finalSubdomain = subdomain; - - if (domainRes.domains.type === "ns") { - if (subdomain) { - fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } else if (domainRes.domains.type === "cname") { - fullDomain = domainRes.domains.baseDomain; - finalSubdomain = null; // CNAME domains don't use subdomains - } else if (domainRes.domains.type === "wildcard") { - if (subdomain !== undefined && subdomain !== null) { - // Validate subdomain format for wildcard domains - const parsedSubdomain = subdomainSchema.safeParse(subdomain); - if (!parsedSubdomain.success) { - return { - success: false, - error: fromError(parsedSubdomain.error).toString() - }; - } - fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`; - } else { - fullDomain = domainRes.domains.baseDomain; - } - } - - // If the full domain equals the base domain, set subdomain to null - if (fullDomain === domainRes.domains.baseDomain) { - finalSubdomain = null; - } - - // Convert to lowercase - fullDomain = fullDomain.toLowerCase(); - - return { - success: true, - fullDomain, - subdomain: finalSubdomain ?? null - }; - } catch (error) { - return { - success: false, - error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}` - }; - } -} diff --git a/server/lib/encryption.ts b/server/lib/encryption.ts deleted file mode 100644 index 7959fa4b..00000000 --- a/server/lib/encryption.ts +++ /dev/null @@ -1,39 +0,0 @@ -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/exitNodes/exitNodeComms.ts b/server/lib/exitNodes/exitNodeComms.ts deleted file mode 100644 index bcfbec3e..00000000 --- a/server/lib/exitNodes/exitNodeComms.ts +++ /dev/null @@ -1,86 +0,0 @@ -import axios from "axios"; -import logger from "@server/logger"; -import { ExitNode } from "@server/db"; - -interface ExitNodeRequest { - remoteType?: string; - localPath: string; - method?: "POST" | "DELETE" | "GET" | "PUT"; - data?: any; - queryParams?: Record; -} - -/** - * Sends a request to an exit node, handling both remote and local exit nodes - * @param exitNode The exit node to send the request to - * @param request The request configuration - * @returns Promise Response data for local nodes, undefined for remote nodes - */ -export async function sendToExitNode( - exitNode: ExitNode, - request: ExitNodeRequest -): Promise { - if (!exitNode.reachableAt) { - throw new Error( - `Exit node with ID ${exitNode.exitNodeId} is not reachable` - ); - } - - // Handle local exit node with HTTP API - const method = request.method || "POST"; - let url = `${exitNode.reachableAt}${request.localPath}`; - - // Add query parameters if provided - if (request.queryParams) { - const params = new URLSearchParams(request.queryParams); - url += `?${params.toString()}`; - } - - try { - let response; - - switch (method) { - case "POST": - response = await axios.post(url, request.data, { - headers: { - "Content-Type": "application/json" - } - }); - break; - case "DELETE": - response = await axios.delete(url); - break; - case "GET": - response = await axios.get(url); - break; - case "PUT": - response = await axios.put(url, request.data, { - headers: { - "Content-Type": "application/json" - } - }); - break; - default: - throw new Error(`Unsupported HTTP method: ${method}`); - } - - logger.info(`Exit node request successful:`, { - method, - url, - status: response.data.status - }); - - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - logger.error( - `Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}` - ); - } else { - logger.error( - `Error making ${method} request for exit node at ${exitNode.reachableAt}: ${error}` - ); - } - throw error; - } -} diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts deleted file mode 100644 index bb269710..00000000 --- a/server/lib/exitNodes/exitNodes.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { db, exitNodes } from "@server/db"; -import logger from "@server/logger"; -import { ExitNodePingResult } from "@server/routers/newt"; -import { eq } from "drizzle-orm"; - -export async function verifyExitNodeOrgAccess( - exitNodeId: number, - orgId: string -) { - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeId)); - - // For any other type, deny access - return { hasAccess: true, exitNode }; -} - -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({ - exitNodeId: exitNodes.exitNodeId, - name: exitNodes.name, - address: exitNodes.address, - endpoint: exitNodes.endpoint, - publicKey: exitNodes.publicKey, - listenPort: exitNodes.listenPort, - reachableAt: exitNodes.reachableAt, - maxConnections: exitNodes.maxConnections, - online: exitNodes.online, - lastPing: exitNodes.lastPing, - type: exitNodes.type, - region: exitNodes.region - }) - .from(exitNodes); - - // Filter the nodes. If there are NO remoteExitNodes then do nothing. If there are then remove all of the non-remoteExitNodes - if (allExitNodes.length === 0) { - logger.warn("No exit nodes found!"); - return []; - } - - return allExitNodes; -} - -export function selectBestExitNode( - pingResults: ExitNodePingResult[] -): ExitNodePingResult | null { - if (!pingResults || pingResults.length === 0) { - logger.warn("No ping results provided"); - return null; - } - - return pingResults[0]; -} - -export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { - return false; -} - -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 deleted file mode 100644 index d895ce42..00000000 --- a/server/lib/exitNodes/getCurrentExitNodeId.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index ba30ccc2..00000000 --- a/server/lib/exitNodes/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./exitNodes"; -export * from "./exitNodeComms"; -export * from "./subnet"; -export * from "./getCurrentExitNodeId"; \ No newline at end of file diff --git a/server/lib/exitNodes/subnet.ts b/server/lib/exitNodes/subnet.ts deleted file mode 100644 index c06f1d05..00000000 --- a/server/lib/exitNodes/subnet.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db, exitNodes } from "@server/db"; -import config from "@server/lib/config"; -import { findNextAvailableCidr } from "@server/lib/ip"; - -export async function getNextAvailableSubnet(): Promise { - // Get all existing subnets from routes table - const existingAddresses = await db - .select({ - address: exitNodes.address - }) - .from(exitNodes); - - const addresses = existingAddresses.map((a) => a.address); - let subnet = findNextAvailableCidr( - addresses, - config.getRawConfig().gerbil.block_size, - config.getRawConfig().gerbil.subnet_group - ); - if (!subnet) { - throw new Error("No available subnets remaining in space"); - } - - // replace the last octet with 1 - subnet = - subnet.split(".").slice(0, 3).join(".") + - ".1" + - "/" + - subnet.split("/")[1]; - return subnet; -} \ No newline at end of file diff --git a/server/lib/geoip.ts b/server/lib/geoip.ts deleted file mode 100644 index ac739fa3..00000000 --- a/server/lib/geoip.ts +++ /dev/null @@ -1,33 +0,0 @@ -import logger from "@server/logger"; -import { maxmindLookup } from "@server/db/maxmind"; - -export async function getCountryCodeForIp( - ip: string -): Promise { - try { - 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 country.iso_code; - } catch (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 077ac6f6..4eea973e 100644 --- a/server/lib/idp/generateRedirectUrl.ts +++ b/server/lib/idp/generateRedirectUrl.ts @@ -1,48 +1,8 @@ -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, baseUrl!).toString(); + const redirectUrl = new URL(redirectPath, dashboardUrl).toString(); return redirectUrl; } diff --git a/server/lib/index.ts b/server/lib/index.ts new file mode 100644 index 00000000..9d2cfb1f --- /dev/null +++ b/server/lib/index.ts @@ -0,0 +1 @@ +export * from "./response"; diff --git a/server/lib/ip.test.ts b/server/lib/ip.test.ts index 67a2faaa..2c2dd057 100644 --- a/server/lib/ip.test.ts +++ b/server/lib/ip.test.ts @@ -4,14 +4,7 @@ import { assertEquals } from "@test/assert"; // Test cases function testFindNextAvailableCidr() { console.log("Running findNextAvailableCidr tests..."); - - // Test 0: Basic IPv4 allocation with a subnet in the wrong range - { - const existing = ["100.90.130.1/30", "100.90.128.4/30"]; - const result = findNextAvailableCidr(existing, 30, "100.90.130.1/24"); - assertEquals(result, "100.90.130.4/30", "Basic IPv4 allocation failed"); - } - + // Test 1: Basic IPv4 allocation { const existing = ["10.0.0.0/16", "10.1.0.0/16"]; @@ -33,12 +26,6 @@ function testFindNextAvailableCidr() { assertEquals(result, null, "No available space test failed"); } - // Test 4: Empty existing - { - const existing: string[] = []; - const result = findNextAvailableCidr(existing, 30, "10.0.0.0/8"); - assertEquals(result, "10.0.0.0/30", "Empty existing test failed"); - } // // Test 4: IPv6 allocation // { // const existing = ["2001:db8::/32", "2001:db8:1::/32"]; diff --git a/server/lib/ip.ts b/server/lib/ip.ts index c929f025..fd6f07ab 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,8 +1,3 @@ -import { db } from "@server/db"; -import { clients, orgs, sites } from "@server/db"; -import { and, eq, isNotNull } from "drizzle-orm"; -import config from "@server/lib/config"; - interface IPRange { start: bigint; end: bigint; @@ -14,7 +9,7 @@ type IPVersion = 4 | 6; * Detects IP version from address string */ function detectIpVersion(ip: string): IPVersion { - return ip.includes(":") ? 6 : 4; + return ip.includes(':') ? 6 : 4; } /** @@ -24,34 +19,34 @@ function ipToBigInt(ip: string): bigint { const version = detectIpVersion(ip); if (version === 4) { - return ip.split(".").reduce((acc, octet) => { - const num = parseInt(octet); - if (isNaN(num) || num < 0 || num > 255) { - throw new Error(`Invalid IPv4 octet: ${octet}`); - } - return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num)); - }, BigInt(0)); + return ip.split('.') + .reduce((acc, octet) => { + const num = parseInt(octet); + if (isNaN(num) || num < 0 || num > 255) { + throw new Error(`Invalid IPv4 octet: ${octet}`); + } + return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num)); + }, BigInt(0)); } else { // Handle IPv6 // Expand :: notation let fullAddress = ip; - if (ip.includes("::")) { - const parts = ip.split("::"); - if (parts.length > 2) - throw new Error("Invalid IPv6 address: multiple :: found"); - const missing = - 8 - (parts[0].split(":").length + parts[1].split(":").length); - const padding = Array(missing).fill("0").join(":"); + if (ip.includes('::')) { + const parts = ip.split('::'); + if (parts.length > 2) throw new Error('Invalid IPv6 address: multiple :: found'); + const missing = 8 - (parts[0].split(':').length + parts[1].split(':').length); + const padding = Array(missing).fill('0').join(':'); fullAddress = `${parts[0]}:${padding}:${parts[1]}`; } - return fullAddress.split(":").reduce((acc, hextet) => { - const num = parseInt(hextet || "0", 16); - if (isNaN(num) || num < 0 || num > 65535) { - throw new Error(`Invalid IPv6 hextet: ${hextet}`); - } - return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num)); - }, BigInt(0)); + return fullAddress.split(':') + .reduce((acc, hextet) => { + const num = parseInt(hextet || '0', 16); + if (isNaN(num) || num < 0 || num > 65535) { + throw new Error(`Invalid IPv6 hextet: ${hextet}`); + } + return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num)); + }, BigInt(0)); } } @@ -65,15 +60,11 @@ function bigIntToIp(num: bigint, version: IPVersion): string { octets.unshift(Number(num & BigInt(255))); num = num >> BigInt(8); } - return octets.join("."); + return octets.join('.'); } else { const hextets: string[] = []; for (let i = 0; i < 8; i++) { - hextets.unshift( - Number(num & BigInt(65535)) - .toString(16) - .padStart(4, "0") - ); + hextets.unshift(Number(num & BigInt(65535)).toString(16).padStart(4, '0')); num = num >> BigInt(16); } // Compress zero sequences @@ -83,7 +74,7 @@ function bigIntToIp(num: bigint, version: IPVersion): string { let currentZeroLength = 0; for (let i = 0; i < hextets.length; i++) { - if (hextets[i] === "0000") { + if (hextets[i] === '0000') { if (currentZeroStart === -1) currentZeroStart = i; currentZeroLength++; if (currentZeroLength > maxZeroLength) { @@ -97,14 +88,12 @@ function bigIntToIp(num: bigint, version: IPVersion): string { } if (maxZeroLength > 1) { - hextets.splice(maxZeroStart, maxZeroLength, ""); - if (maxZeroStart === 0) hextets.unshift(""); - if (maxZeroStart + maxZeroLength === 8) hextets.push(""); + hextets.splice(maxZeroStart, maxZeroLength, ''); + if (maxZeroStart === 0) hextets.unshift(''); + if (maxZeroStart + maxZeroLength === 8) hextets.push(''); } - return hextets - .map((h) => (h === "0000" ? "0" : h.replace(/^0+/, ""))) - .join(":"); + return hextets.map(h => h === '0000' ? '0' : h.replace(/^0+/, '')).join(':'); } } @@ -112,7 +101,7 @@ function bigIntToIp(num: bigint, version: IPVersion): string { * Converts CIDR to IP range */ export function cidrToRange(cidr: string): IPRange { - const [ip, prefix] = cidr.split("/"); + const [ip, prefix] = cidr.split('/'); const version = detectIpVersion(ip); const prefixBits = parseInt(prefix); const ipBigInt = ipToBigInt(ip); @@ -124,10 +113,7 @@ export function cidrToRange(cidr: string): IPRange { } const shiftBits = BigInt(maxPrefix - prefixBits); - const mask = BigInt.asUintN( - version === 4 ? 64 : 128, - (BigInt(1) << shiftBits) - BigInt(1) - ); + const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1)); const start = ipBigInt & ~mask; const end = start | mask; @@ -146,32 +132,28 @@ export function findNextAvailableCidr( blockSize: number, startCidr?: string ): string | null { + if (!startCidr && existingCidrs.length === 0) { return null; } // If no existing CIDRs, use the IP version from startCidr - const version = startCidr ? detectIpVersion(startCidr.split("/")[0]) : 4; // Default to IPv4 if no startCidr provided + const version = startCidr + ? detectIpVersion(startCidr.split('/')[0]) + : 4; // Default to IPv4 if no startCidr provided // Use appropriate default startCidr if none provided startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0"); // If there are existing CIDRs, ensure all are same version - if ( - existingCidrs.length > 0 && - existingCidrs.some( - (cidr) => detectIpVersion(cidr.split("/")[0]) !== version - ) - ) { - throw new Error("All CIDRs must be of the same IP version"); + if (existingCidrs.length > 0 && + existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) { + throw new Error('All CIDRs must be of the same IP version'); } - // Extract the network part from startCidr to ensure we stay in the right subnet - const startCidrRange = cidrToRange(startCidr); - // Convert existing CIDRs to ranges and sort them const existingRanges = existingCidrs - .map((cidr) => cidrToRange(cidr)) + .map(cidr => cidrToRange(cidr)) .sort((a, b) => (a.start < b.start ? -1 : 1)); // Calculate block size @@ -179,17 +161,14 @@ export function findNextAvailableCidr( const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize); // Start from the beginning of the given CIDR - let current = startCidrRange.start; - const maxIp = startCidrRange.end; + let current = cidrToRange(startCidr).start; + const maxIp = cidrToRange(startCidr).end; // Iterate through existing ranges for (let i = 0; i <= existingRanges.length; i++) { const nextRange = existingRanges[i]; - // Align current to block size - const alignedCurrent = - current + - ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt); + const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt); // Check if we've gone beyond the maximum allowed IP if (alignedCurrent + blockSizeBigInt - BigInt(1) > maxIp) { @@ -197,18 +176,12 @@ export function findNextAvailableCidr( } // If we're at the end of existing ranges or found a gap - if ( - !nextRange || - alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start - ) { + if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) { return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`; } - // If next range overlaps with our search space, move past it - if (nextRange.end >= startCidrRange.start && nextRange.start <= maxIp) { - // Move current pointer to after the current range - current = nextRange.end + BigInt(1); - } + // Move current pointer to after the current range + current = nextRange.end + BigInt(1); } return null; @@ -222,7 +195,7 @@ export function findNextAvailableCidr( */ export function isIpInCidr(ip: string, cidr: string): boolean { const ipVersion = detectIpVersion(ip); - const cidrVersion = detectIpVersion(cidr.split("/")[0]); + const cidrVersion = detectIpVersion(cidr.split('/')[0]); // If IP versions don't match, the IP cannot be in the CIDR range if (ipVersion !== cidrVersion) { @@ -234,69 +207,3 @@ export function isIpInCidr(ip: string, cidr: string): boolean { const range = cidrToRange(cidr); return ipBigInt >= range.start && ipBigInt <= range.end; } - -export async function getNextAvailableClientSubnet( - orgId: string -): Promise { - const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); - - if (!org) { - throw new Error(`Organization with ID ${orgId} not found`); - } - - if (!org.subnet) { - throw new Error(`Organization with ID ${orgId} has no subnet defined`); - } - - const existingAddressesSites = await db - .select({ - address: sites.address - }) - .from(sites) - .where(and(isNotNull(sites.address), eq(sites.orgId, orgId))); - - const existingAddressesClients = await db - .select({ - address: clients.subnet - }) - .from(clients) - .where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId))); - - const addresses = [ - ...existingAddressesSites.map( - (site) => `${site.address?.split("/")[0]}/32` - ), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org - ...existingAddressesClients.map( - (client) => `${client.address.split("/")}/32` - ) - ].filter((address) => address !== null) as string[]; - - const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org - if (!subnet) { - throw new Error("No available subnets remaining in space"); - } - - return subnet; -} - -export async function getNextAvailableOrgSubnet(): Promise { - const existingAddresses = await db - .select({ - subnet: orgs.subnet - }) - .from(orgs) - .where(isNotNull(orgs.subnet)); - - const addresses = existingAddresses.map((org) => org.subnet!); - - const subnet = findNextAvailableCidr( - addresses, - config.getRawConfig().orgs.block_size, - config.getRawConfig().orgs.subnet_group - ); - if (!subnet) { - throw new Error("No available subnets remaining in space"); - } - - return subnet; -} diff --git a/server/lib/rateLimitStore.ts b/server/lib/rateLimitStore.ts deleted file mode 100644 index 56adad98..00000000 --- a/server/lib/rateLimitStore.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MemoryStore, Store } from "express-rate-limit"; - -export function createStore(): Store { - const rateLimitStore: Store = new MemoryStore(); - return rateLimitStore; -} diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts deleted file mode 100644 index 6d57399d..00000000 --- a/server/lib/readConfigFile.ts +++ /dev/null @@ -1,391 +0,0 @@ -import fs from "fs"; -import yaml from "js-yaml"; -import { configFilePath1, configFilePath2 } from "./consts"; -import { z } from "zod"; -import stoi from "./stoi"; - -const portSchema = z.number().positive().gt(0).lte(65535); - -const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { - return process.env[envVar] ?? valFromYaml; -}; - -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 - }), - domains: z - .record( - z.string(), - z.object({ - base_domain: z - .string() - .nonempty("base_domain must not be empty") - .transform((url) => url.toLowerCase()), - cert_resolver: z.string().optional().default("letsencrypt"), - prefer_wildcard_cert: z.boolean().optional().default(false) - }) - ) - .optional(), - 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().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 - .object({ - http_entrypoint: z.string().optional().default("web"), - https_entrypoint: z.string().optional().default("websecure"), - additional_middlewares: z.array(z.string()).optional(), - cert_resolver: z.string().optional().default("letsencrypt"), - prefer_wildcard_cert: z.boolean().optional().default(false), - certificates_path: z.string().default("/var/certificates"), - monitor_interval: z.number().default(5000), - dynamic_cert_config_path: z - .string() - .optional() - .default("/var/dynamic/cert_config.yml"), - dynamic_router_config_path: z - .string() - .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"]), - allow_raw_resources: z.boolean().optional().default(true), - file_mode: z.boolean().optional().default(false) - }) - .optional() - .default({}), - gerbil: z - .object({ - exit_node_name: z.string().optional(), - start_port: portSchema - .optional() - .default(51820) - .transform(stoi) - .pipe(portSchema), - base_endpoint: z - .string() - .optional() - .pipe(z.string()) - .transform((url) => url.toLowerCase()), - use_subdomain: z.boolean().optional().default(false), - subnet_group: z.string().optional().default("100.89.137.0/20"), - block_size: z.number().positive().gt(0).optional().default(24), - site_block_size: z - .number() - .positive() - .gt(0) - .optional() - .default(30) - }) - .optional() - .default({}), - orgs: z - .object({ - block_size: z.number().positive().gt(0).optional().default(24), - subnet_group: z.string().optional().default("100.90.128.0/24") - }) - .optional() - .default({ - block_size: 24, - subnet_group: "100.90.128.0/24" - }), - rate_limits: z - .object({ - global: z - .object({ - window_minutes: z - .number() - .positive() - .gt(0) - .optional() - .default(1), - max_requests: z - .number() - .positive() - .gt(0) - .optional() - .default(500) - }) - .optional() - .default({}), - auth: z - .object({ - window_minutes: z - .number() - .positive() - .gt(0) - .optional() - .default(1), - max_requests: z - .number() - .positive() - .gt(0) - .optional() - .default(500) - }) - .optional() - .default({}) - }) - .optional() - .default({}), - email: z - .object({ - smtp_host: z.string().optional(), - smtp_port: portSchema.optional(), - smtp_user: z.string().optional(), - smtp_pass: z - .string() - .optional() - .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), - smtp_secure: z.boolean().optional(), - smtp_tls_reject_unauthorized: z.boolean().optional(), - no_reply: z.string().email().optional() - }) - .optional(), - flags: z - .object({ - require_email_verification: z.boolean().optional(), - disable_signup_without_invite: z.boolean().optional(), - disable_user_create_org: z.boolean().optional(), - allow_raw_resources: z.boolean().optional(), - enable_integration_api: z.boolean().optional(), - disable_local_sites: z.boolean().optional(), - disable_basic_wireguard_sites: z.boolean().optional(), - disable_config_managed_domains: z.boolean().optional(), - enable_clients: z.boolean().optional().default(true) - }) - .optional(), - dns: z - .object({ - nameservers: z - .array(z.string().optional().optional()) - .optional() - .default(["ns1.fossorial.io", "ns2.fossorial.io"]), - cname_extension: z.string().optional().default("fossorial.io") - }) - .optional() - .default({ - nameservers: ["ns1.fossorial.io", "ns2.fossorial.io"], - cname_extension: "fossorial.io" - }) - }) - .refine( - (data) => { - const keys = Object.keys(data.domains || {}); - if (data.flags?.disable_config_managed_domains) { - return true; - } - - if (keys.length === 0) { - return false; - } - return true; - }, - { - message: "At least one domain must be defined" - } - ) - .refine( - (data) => { - // 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 - ); - }, - { - message: "Server secret must be defined" - } - ) - .refine( - (data) => { - // If hybrid is not defined, dashboard_url must be defined - return ( - data.app.dashboard_url !== undefined && - data.app.dashboard_url.length > 0 - ); - }, - { - message: "Dashboard URL must be defined" - } - ); - -export function readConfigFile() { - const loadConfig = (configPath: string) => { - try { - const yamlContent = fs.readFileSync(configPath, "utf8"); - const config = yaml.load(yamlContent); - return config; - } catch (error) { - if (error instanceof Error) { - throw new Error( - `Error loading configuration file: ${error.message}` - ); - } - throw error; - } - }; - - let environment: any; - if (fs.existsSync(configFilePath1)) { - environment = loadConfig(configFilePath1); - } else if (fs.existsSync(configFilePath2)) { - environment = loadConfig(configFilePath2); - } - - if (!environment) { - throw new Error( - "No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file" - ); - } - - return environment; -} diff --git a/server/lib/resend.ts b/server/lib/resend.ts deleted file mode 100644 index 7dd130c8..00000000 --- a/server/lib/resend.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 5e2bd400..cf1b40c8 100644 --- a/server/lib/schemas.ts +++ b/server/lib/schemas.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const subdomainSchema = z .string() .regex( - /^(?!:\/\/)([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/, + /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/, "Invalid subdomain format" ) .min(1, "Subdomain must be at least 1 character long") @@ -12,17 +12,7 @@ export const subdomainSchema = z export const tlsNameSchema = z .string() .regex( - /^(?!:\/\/)([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$|^$/, + /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$|^$/, "Invalid subdomain format" ) - .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()); + .transform((val) => val.toLowerCase()); \ No newline at end of file diff --git a/server/lib/stoi.ts b/server/lib/stoi.ts index ebc789e6..8fa42b54 100644 --- a/server/lib/stoi.ts +++ b/server/lib/stoi.ts @@ -1,6 +1,6 @@ export default function stoi(val: any) { if (typeof val === "string") { - return parseInt(val); + return parseInt(val) } else { return val; diff --git a/server/lib/totp.ts b/server/lib/totp.ts deleted file mode 100644 index d9f819ab..00000000 --- a/server/lib/totp.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { alphabet, generateRandomString } from "oslo/crypto"; - -export async function generateBackupCodes(): Promise { - const codes = []; - for (let i = 0; i < 10; i++) { - const code = generateRandomString(6, alphabet("0-9", "A-Z", "a-z")); - codes.push(code); - } - return codes; -} diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts deleted file mode 100644 index 030ad4ab..00000000 --- a/server/lib/traefik/TraefikConfigManager.ts +++ /dev/null @@ -1,973 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import config from "@server/lib/config"; -import logger from "@server/logger"; -import * as yaml from "js-yaml"; -import axios from "axios"; -import { db, exitNodes } from "@server/db"; -import { eq } from "drizzle-orm"; -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; - private isRunning = false; - private activeDomains = new Set(); - private timeoutId: NodeJS.Timeout | null = null; - private lastCertificateFetch: Date | null = null; - private lastKnownDomains = new Set(); - private lastLocalCertificateState = new Map< - string, - { - exists: boolean; - lastModified: number | null; - expiresAt: number | null; - wildcard: boolean | null; - } - >(); - - constructor() {} - - /** - * Start monitoring certificates - */ - private scheduleNextExecution(): void { - const intervalMs = config.getRawConfig().traefik.monitor_interval; - const now = Date.now(); - const nextExecution = Math.ceil(now / intervalMs) * intervalMs; - const delay = nextExecution - now; - - this.timeoutId = setTimeout(async () => { - try { - await this.HandleTraefikConfig(); - } catch (error) { - logger.error("Error during certificate monitoring:", error); - } - - if (this.isRunning) { - this.scheduleNextExecution(); // Schedule the next execution - } - }, delay); - } - - async start(): Promise { - if (this.isRunning) { - logger.info("Certificate monitor is already running"); - return; - } - this.isRunning = true; - logger.info(`Starting certificate monitor for exit node`); - - // Ensure certificates directory exists - await this.ensureDirectoryExists( - config.getRawConfig().traefik.certificates_path - ); - - // Initialize local certificate state - this.lastLocalCertificateState = await this.scanLocalCertificateState(); - logger.info( - `Found ${this.lastLocalCertificateState.size} existing certificate directories` - ); - - // Run initial check - await this.HandleTraefikConfig(); - - // Start synchronized scheduling - this.scheduleNextExecution(); - - logger.info( - `Certificate monitor started with synchronized ${ - config.getRawConfig().traefik.monitor_interval - }ms interval` - ); - } - /** - * Stop monitoring certificates - */ - stop(): void { - if (!this.isRunning) { - logger.info("Certificate monitor is not running"); - return; - } - - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = null; - } - - this.isRunning = false; - logger.info("Certificate monitor stopped"); - } - - /** - * Scan local certificate directories to build current state - */ - private async scanLocalCertificateState(): Promise< - Map< - string, - { - exists: boolean; - lastModified: number | null; - expiresAt: number | null; - wildcard: boolean; - } - > - > { - const state = new Map(); - const certsPath = config.getRawConfig().traefik.certificates_path; - - try { - if (!fs.existsSync(certsPath)) { - return state; - } - - const certDirs = fs.readdirSync(certsPath, { withFileTypes: true }); - - for (const dirent of certDirs) { - if (!dirent.isDirectory()) continue; - - const domain = dirent.name; - const domainDir = path.join(certsPath, domain); - const certPath = path.join(domainDir, "cert.pem"); - const keyPath = path.join(domainDir, "key.pem"); - const lastUpdatePath = path.join(domainDir, ".last_update"); - const wildcardPath = path.join(domainDir, ".wildcard"); - - const certExists = await this.fileExists(certPath); - const keyExists = await this.fileExists(keyPath); - const lastUpdateExists = await this.fileExists(lastUpdatePath); - const wildcardExists = await this.fileExists(wildcardPath); - - let lastModified: Date | null = null; - const expiresAt: Date | null = null; - let wildcard = false; - - if (lastUpdateExists) { - try { - const lastUpdateStr = fs - .readFileSync(lastUpdatePath, "utf8") - .trim(); - lastModified = new Date(lastUpdateStr); - } catch { - // If we can't read the last update, fall back to file stats - try { - const stats = fs.statSync(certPath); - lastModified = stats.mtime; - } catch { - lastModified = null; - } - } - } - - // Check if this is a wildcard certificate - if (wildcardExists) { - try { - const wildcardContent = fs - .readFileSync(wildcardPath, "utf8") - .trim(); - wildcard = wildcardContent === "true"; - } catch (error) { - logger.warn( - `Could not read wildcard file for ${domain}:`, - error - ); - } - } - - state.set(domain, { - exists: certExists && keyExists, - lastModified, - expiresAt, - wildcard - }); - } - } catch (error) { - logger.error("Error scanning local certificate state:", error); - } - - return state; - } - - /** - * Check if we need to fetch certificates from remote - */ - private shouldFetchCertificates(currentDomains: Set): boolean { - // Always fetch on first run - if (!this.lastCertificateFetch) { - return true; - } - - // Fetch if it's been more than 24 hours (for renewals) - const dayInMs = 24 * 60 * 60 * 1000; - const timeSinceLastFetch = - Date.now() - this.lastCertificateFetch.getTime(); - if (timeSinceLastFetch > dayInMs) { - logger.info("Fetching certificates due to 24-hour renewal check"); - return true; - } - - // Filter out domains covered by wildcard certificates - const domainsNeedingCerts = new Set(); - for (const domain of currentDomains) { - if ( - !isDomainCoveredByWildcard( - domain, - this.lastLocalCertificateState - ) - ) { - domainsNeedingCerts.add(domain); - } - } - - // Fetch if domains needing certificates have changed - const lastDomainsNeedingCerts = new Set(); - for (const domain of this.lastKnownDomains) { - if ( - !isDomainCoveredByWildcard( - domain, - this.lastLocalCertificateState - ) - ) { - lastDomainsNeedingCerts.add(domain); - } - } - - if ( - domainsNeedingCerts.size !== lastDomainsNeedingCerts.size || - !Array.from(domainsNeedingCerts).every((domain) => - lastDomainsNeedingCerts.has(domain) - ) - ) { - logger.info( - "Fetching certificates due to domain changes (after wildcard filtering)" - ); - return true; - } - - // Check if any local certificates are missing or appear to be outdated - for (const domain of domainsNeedingCerts) { - const localState = this.lastLocalCertificateState.get(domain); - if (!localState || !localState.exists) { - logger.info( - `Fetching certificates due to missing local cert for ${domain}` - ); - return true; - } - - // Check if certificate is expiring soon (within 30 days) - if (localState.expiresAt) { - const daysUntilExpiry = - (localState.expiresAt - Math.floor(Date.now() / 1000)) / - (1000 * 60 * 60 * 24); - if (daysUntilExpiry < 30) { - logger.info( - `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` - ); - return true; - } - } - } - - return false; - } - - /** - * Main monitoring logic - */ - lastActiveDomains: Set = new Set(); - public async HandleTraefikConfig(): Promise { - try { - // Get all active domains for this exit node via HTTP call - const getTraefikConfig = await this.internalGetTraefikConfig(); - - if (!getTraefikConfig) { - logger.error( - "Failed to fetch active domains from traefik config" - ); - return; - } - - const { domains, traefikConfig } = getTraefikConfig; - - // Add static domains from config - // const staticDomains = [config.getRawConfig().app.dashboard_url]; - // staticDomains.forEach((domain) => domains.add(domain)); - - // Log if domains changed - if ( - this.lastActiveDomains.size !== domains.size || - !Array.from(this.lastActiveDomains).every((domain) => - domains.has(domain) - ) - ) { - logger.info( - `Active domains changed for exit node: ${Array.from(domains).join(", ")}` - ); - this.lastActiveDomains = new Set(domains); - } - - // Write traefik config as YAML to a second dynamic config file if changed - await this.writeTraefikDynamicConfig(traefikConfig); - - // Send domains to SNI proxy - try { - let exitNode; - if (config.getRawConfig().gerbil.exit_node_name) { - const exitNodeName = - config.getRawConfig().gerbil.exit_node_name!; - [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.name, exitNodeName)) - .limit(1); - } else { - [exitNode] = await db.select().from(exitNodes).limit(1); - } - if (exitNode) { - await sendToExitNode(exitNode, { - localPath: "/update-local-snis", - method: "POST", - data: { fullDomains: Array.from(domains) } - }); - } else { - logger.error( - "No exit node found. Has gerbil registered yet?" - ); - } - } catch (err) { - logger.error("Failed to post domains to SNI proxy:", err); - } - - // Update active domains tracking - this.activeDomains = domains; - } catch (error) { - logger.error("Error in traefik config monitoring cycle:", error); - } - } - - /** - * Get all domains currently in use from traefik config API - */ - private async internalGetTraefikConfig(): Promise<{ - domains: Set; - traefikConfig: any; - } | null> { - let traefikConfig; - try { - 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(); - - if (traefikConfig?.http?.routers) { - for (const router of Object.values( - traefikConfig.http.routers - )) { - if (router.rule && typeof router.rule === "string") { - // Match Host(`domain`) - const match = router.rule.match(/Host\(`([^`]+)`\)/); - if (match && match[1]) { - domains.add(match[1]); - } - } - } - } - - // logger.debug( - // `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}` - // ); - - const badgerMiddlewareName = "badger"; - if (traefikConfig?.http?.middlewares) { - traefikConfig.http.middlewares[badgerMiddlewareName] = { - plugin: { - [badgerMiddlewareName]: { - apiBaseUrl: new URL( - "/api/v1", - `http://${ - config.getRawConfig().server - .internal_hostname - }:${config.getRawConfig().server.internal_port}` - ).href, - userSessionCookieName: - config.getRawConfig().server - .session_cookie_name, - - // deprecated - accessTokenQueryParam: - config.getRawConfig().server - .resource_access_token_param, - - resourceSessionRequestParam: - config.getRawConfig().server - .resource_session_request_param - } - } - }; - } - - return { domains, traefikConfig }; - } catch (error) { - // pull data out of the axios error to log - if (axios.isAxiosError(error)) { - logger.error("Error fetching traefik config:", { - message: error.message, - code: error.code, - status: error.response?.status, - statusText: error.response?.statusText, - url: error.config?.url, - method: error.config?.method - }); - } else { - logger.error("Error fetching traefik config:", error); - } - return null; - } - } - - /** - * Write traefik config as YAML to a second dynamic config file if changed - */ - private async writeTraefikDynamicConfig(traefikConfig: any): Promise { - const traefikDynamicConfigPath = - config.getRawConfig().traefik.dynamic_router_config_path; - let shouldWrite = false; - let oldJson = ""; - if (fs.existsSync(traefikDynamicConfigPath)) { - try { - const oldContent = fs.readFileSync( - traefikDynamicConfigPath, - "utf8" - ); - // Try to parse as YAML then JSON.stringify for comparison - const oldObj = yaml.load(oldContent); - oldJson = JSON.stringify(oldObj); - } catch { - oldJson = ""; - } - } - const newJson = JSON.stringify(traefikConfig); - if (oldJson !== newJson) { - shouldWrite = true; - } - if (shouldWrite) { - try { - fs.writeFileSync( - traefikDynamicConfigPath, - yaml.dump(traefikConfig, { noRefs: true }), - "utf8" - ); - logger.info("Traefik dynamic config updated"); - } catch (err) { - logger.error("Failed to write traefik dynamic config:", err); - } - } - } - - /** - * Update dynamic config from existing local certificates without fetching from remote - */ - private async updateDynamicConfigFromLocalCerts( - domains: Set - ): Promise { - const dynamicConfigPath = - config.getRawConfig().traefik.dynamic_cert_config_path; - - // Load existing dynamic config if it exists, otherwise initialize - let dynamicConfig: any = { tls: { certificates: [] } }; - if (fs.existsSync(dynamicConfigPath)) { - try { - const fileContent = fs.readFileSync(dynamicConfigPath, "utf8"); - dynamicConfig = yaml.load(fileContent) || dynamicConfig; - if (!dynamicConfig.tls) - dynamicConfig.tls = { certificates: [] }; - if (!Array.isArray(dynamicConfig.tls.certificates)) { - dynamicConfig.tls.certificates = []; - } - } catch (err) { - logger.error("Failed to load existing dynamic config:", err); - } - } - - // Keep a copy of the original config for comparison - const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); - - // Clear existing certificates and rebuild from local state - dynamicConfig.tls.certificates = []; - - // Keep track of certificates we've already added to avoid duplicates - const addedCertPaths = new Set(); - - for (const domain of domains) { - // First, try to find an exact match certificate - const localState = this.lastLocalCertificateState.get(domain); - if (localState && localState.exists) { - const domainDir = path.join( - config.getRawConfig().traefik.certificates_path, - domain - ); - const certPath = path.join(domainDir, "cert.pem"); - const keyPath = path.join(domainDir, "key.pem"); - - if (!addedCertPaths.has(certPath)) { - const certEntry = { - certFile: certPath, - keyFile: keyPath - }; - dynamicConfig.tls.certificates.push(certEntry); - addedCertPaths.add(certPath); - } - continue; - } - - // If no exact match, check for wildcard certificates that cover this domain - for (const [certDomain, certState] of this - .lastLocalCertificateState) { - if (certState.exists && certState.wildcard) { - // Check if this wildcard certificate covers the domain - if (domain.endsWith("." + certDomain)) { - // Verify it's only one level deep (wildcard only covers one level) - const prefix = domain.substring( - 0, - domain.length - ("." + certDomain).length - ); - if (!prefix.includes(".")) { - const domainDir = path.join( - config.getRawConfig().traefik.certificates_path, - certDomain - ); - const certPath = path.join(domainDir, "cert.pem"); - const keyPath = path.join(domainDir, "key.pem"); - - if (!addedCertPaths.has(certPath)) { - const certEntry = { - certFile: certPath, - keyFile: keyPath - }; - dynamicConfig.tls.certificates.push(certEntry); - addedCertPaths.add(certPath); - } - break; // Found a wildcard that covers this domain - } - } - } - } - } - - // Only write the config if it has changed - const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); - if (newConfigYaml !== originalConfigYaml) { - fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); - logger.info("Dynamic cert config updated from local certificates"); - } - } - - /** - * Process valid certificates - download and decrypt them - */ - private async processValidCertificates( - validCertificates: Array<{ - id: number; - domain: string; - wildcard: boolean | null; - certFile: string | null; - keyFile: string | null; - expiresAt: number | null; - updatedAt?: number | null; - }> - ): Promise { - const dynamicConfigPath = - config.getRawConfig().traefik.dynamic_cert_config_path; - - // Load existing dynamic config if it exists, otherwise initialize - let dynamicConfig: any = { tls: { certificates: [] } }; - if (fs.existsSync(dynamicConfigPath)) { - try { - const fileContent = fs.readFileSync(dynamicConfigPath, "utf8"); - dynamicConfig = yaml.load(fileContent) || dynamicConfig; - if (!dynamicConfig.tls) - dynamicConfig.tls = { certificates: [] }; - if (!Array.isArray(dynamicConfig.tls.certificates)) { - dynamicConfig.tls.certificates = []; - } - } catch (err) { - logger.error("Failed to load existing dynamic config:", err); - } - } - - // Keep a copy of the original config for comparison - const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); - - for (const cert of validCertificates) { - try { - if ( - !cert.certFile || - !cert.keyFile || - cert.certFile.length === 0 || - cert.keyFile.length === 0 - ) { - logger.warn( - `Certificate for domain ${cert.domain} is missing cert or key file` - ); - continue; - } - - const domainDir = path.join( - config.getRawConfig().traefik.certificates_path, - cert.domain - ); - await this.ensureDirectoryExists(domainDir); - - const certPath = path.join(domainDir, "cert.pem"); - const keyPath = path.join(domainDir, "key.pem"); - const lastUpdatePath = path.join(domainDir, ".last_update"); - - // Check if we need to update the certificate - const shouldUpdate = await this.shouldUpdateCertificate( - cert, - certPath, - keyPath, - lastUpdatePath - ); - - if (shouldUpdate) { - logger.info( - `Processing certificate for domain: ${cert.domain}` - ); - - fs.writeFileSync(certPath, cert.certFile, "utf8"); - fs.writeFileSync(keyPath, cert.keyFile, "utf8"); - - // Set appropriate permissions (readable by owner only for key file) - fs.chmodSync(certPath, 0o644); - fs.chmodSync(keyPath, 0o600); - - // Write/update .last_update file with current timestamp - fs.writeFileSync( - lastUpdatePath, - new Date().toISOString(), - "utf8" - ); - - // Check if this is a wildcard certificate and store it - const wildcardPath = path.join(domainDir, ".wildcard"); - fs.writeFileSync( - wildcardPath, - cert.wildcard ? "true" : "false", - "utf8" - ); - - logger.info( - `Certificate updated for domain: ${cert.domain}${cert.wildcard ? " (wildcard)" : ""}` - ); - - // Update local state tracking - this.lastLocalCertificateState.set(cert.domain, { - exists: true, - lastModified: Math.floor(Date.now() / 1000), - expiresAt: cert.expiresAt, - wildcard: cert.wildcard - }); - } - - // Always ensure the config entry exists and is up to date - const certEntry = { - certFile: certPath, - keyFile: keyPath - }; - // Remove any existing entry for this cert/key path - dynamicConfig.tls.certificates = - dynamicConfig.tls.certificates.filter( - (entry: any) => - entry.certFile !== certEntry.certFile || - entry.keyFile !== certEntry.keyFile - ); - dynamicConfig.tls.certificates.push(certEntry); - } catch (error) { - logger.error( - `Error processing certificate for domain ${cert.domain}:`, - error - ); - } - } - - // Only write the config if it has changed - const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); - if (newConfigYaml !== originalConfigYaml) { - fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); - logger.info("Dynamic cert config updated"); - } - } - - /** - * Check if certificate should be updated - */ - private async shouldUpdateCertificate( - cert: { - id: number; - domain: string; - expiresAt: number | null; - updatedAt?: number | null; - }, - certPath: string, - keyPath: string, - lastUpdatePath: string - ): Promise { - try { - // If files don't exist, we need to create them - const certExists = await this.fileExists(certPath); - const keyExists = await this.fileExists(keyPath); - const lastUpdateExists = await this.fileExists(lastUpdatePath); - - if (!certExists || !keyExists || !lastUpdateExists) { - return true; - } - - // Read last update time from .last_update file - let lastUpdateTime: number | null = null; - try { - const lastUpdateStr = fs - .readFileSync(lastUpdatePath, "utf8") - .trim(); - lastUpdateTime = Math.floor( - new Date(lastUpdateStr).getTime() / 1000 - ); - } catch { - lastUpdateTime = null; - } - - // Use updatedAt from cert, fallback to expiresAt if not present - const dbUpdateTime = cert.updatedAt ?? cert.expiresAt; - - if (!dbUpdateTime) { - // If no update time in DB, always update - return true; - } - - // If DB updatedAt is newer than last update file, update - if (!lastUpdateTime || dbUpdateTime > lastUpdateTime) { - return true; - } - - return false; - } catch (error) { - logger.error( - `Error checking certificate update status for ${cert.domain}:`, - error - ); - return true; // When in doubt, update - } - } - - /** - * Clean up certificates for domains no longer in use - */ - private async cleanupUnusedCertificates( - currentActiveDomains: Set - ): Promise { - try { - const certsPath = config.getRawConfig().traefik.certificates_path; - const dynamicConfigPath = - config.getRawConfig().traefik.dynamic_cert_config_path; - - // Load existing dynamic config if it exists - let dynamicConfig: any = { tls: { certificates: [] } }; - if (fs.existsSync(dynamicConfigPath)) { - try { - const fileContent = fs.readFileSync( - dynamicConfigPath, - "utf8" - ); - dynamicConfig = yaml.load(fileContent) || dynamicConfig; - if (!dynamicConfig.tls) - dynamicConfig.tls = { certificates: [] }; - if (!Array.isArray(dynamicConfig.tls.certificates)) { - dynamicConfig.tls.certificates = []; - } - } catch (err) { - logger.error( - "Failed to load existing dynamic config:", - err - ); - } - } - - const certDirs = fs.readdirSync(certsPath, { - withFileTypes: true - }); - - let configChanged = false; - - for (const dirent of certDirs) { - if (!dirent.isDirectory()) continue; - - const dirName = dirent.name; - // Only delete if NO current domain is exactly the same or ends with `.${dirName}` - const shouldDelete = !Array.from(currentActiveDomains).some( - (domain) => - domain === dirName || domain.endsWith(`.${dirName}`) - ); - - if (shouldDelete) { - const domainDir = path.join(certsPath, dirName); - logger.info( - `Cleaning up unused certificate directory: ${dirName}` - ); - fs.rmSync(domainDir, { recursive: true, force: true }); - - // Remove from local state tracking - this.lastLocalCertificateState.delete(dirName); - - // Remove from dynamic config - const certFilePath = path.join(domainDir, "cert.pem"); - const keyFilePath = path.join(domainDir, "key.pem"); - const before = dynamicConfig.tls.certificates.length; - dynamicConfig.tls.certificates = - dynamicConfig.tls.certificates.filter( - (entry: any) => - entry.certFile !== certFilePath && - entry.keyFile !== keyFilePath - ); - if (dynamicConfig.tls.certificates.length !== before) { - configChanged = true; - } - } - } - - if (configChanged) { - try { - fs.writeFileSync( - dynamicConfigPath, - yaml.dump(dynamicConfig, { noRefs: true }), - "utf8" - ); - logger.info("Dynamic config updated after cleanup"); - } catch (err) { - logger.error( - "Failed to update dynamic config after cleanup:", - err - ); - } - } - } catch (error) { - logger.error("Error during certificate cleanup:", error); - } - } - - /** - * Ensure directory exists - */ - private async ensureDirectoryExists(dirPath: string): Promise { - try { - fs.mkdirSync(dirPath, { recursive: true }); - } catch (error) { - logger.error(`Error creating directory ${dirPath}:`, error); - throw error; - } - } - - /** - * Check if file exists - */ - private async fileExists(filePath: string): Promise { - try { - fs.accessSync(filePath); - return true; - } catch { - return false; - } - } - - /** - * Force a certificate refresh regardless of cache state - */ - public async forceCertificateRefresh(): Promise { - logger.info("Forcing certificate refresh"); - this.lastCertificateFetch = null; - this.lastKnownDomains = new Set(); - await this.HandleTraefikConfig(); - } - - /** - * Get current status - */ - getStatus(): { - isRunning: boolean; - activeDomains: string[]; - monitorInterval: number; - lastCertificateFetch: Date | null; - localCertificateCount: number; - wildcardCertificates: string[]; - domainsCoveredByWildcards: string[]; - } { - const wildcardCertificates: string[] = []; - const domainsCoveredByWildcards: string[] = []; - - // Find wildcard certificates - for (const [domain, state] of this.lastLocalCertificateState) { - if (state.exists && state.wildcard) { - wildcardCertificates.push(domain); - } - } - - // Find domains covered by wildcards - for (const domain of this.activeDomains) { - if ( - isDomainCoveredByWildcard( - domain, - this.lastLocalCertificateState - ) - ) { - domainsCoveredByWildcards.push(domain); - } - } - - return { - isRunning: this.isRunning, - activeDomains: Array.from(this.activeDomains), - monitorInterval: - config.getRawConfig().traefik.monitor_interval || 5000, - lastCertificateFetch: this.lastCertificateFetch, - localCertificateCount: this.lastLocalCertificateState.size, - wildcardCertificates, - domainsCoveredByWildcards - }; - } -} - -/** - * Check if a domain is covered by existing wildcard certificates - */ -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 - if (domain.endsWith("." + certDomain)) { - // Check that it's only one level deep (wildcard only covers one level) - const prefix = domain.substring( - 0, - domain.length - ("." + certDomain).length - ); - // If prefix contains a dot, it's more than one level deep - if (!prefix.includes(".")) { - return true; - } - } - } - } - return false; -} diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts deleted file mode 100644 index cf4d5d42..00000000 --- a/server/lib/traefik/getTraefikConfig.ts +++ /dev/null @@ -1,632 +0,0 @@ -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 config from "@server/lib/config"; -import { resources, sites, Target, targets } from "@server/db"; -import createPathRewriteMiddleware from "./middleware"; -import { sanitize, validatePathRewriteConfig } from "./utils"; - -const redirectHttpsMiddlewareName = "redirect-to-https"; -const badgerMiddlewareName = "badger"; - -export async function getTraefikConfig( - exitNodeId: number, - siteTypes: string[], - filterOutNamespaceDomains = false, - generateLoginPageRouters = false -): Promise { - // Define extended target type with site information - type TargetWithSite = Target & { - site: { - siteId: number; - type: string; - subnet: string | null; - exitNodeId: number | null; - online: boolean; - }; - }; - - // Get resources with their targets and sites in a single optimized query - // Start from sites on this exit node, then join to targets and resources - const resourcesWithTargetsAndSites = await db - .select({ - // Resource fields - resourceId: resources.resourceId, - resourceName: resources.name, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol, - subdomain: resources.subdomain, - domainId: resources.domainId, - enabled: resources.enabled, - stickySession: resources.stickySession, - tlsServerName: resources.tlsServerName, - setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy, - headers: resources.headers, - // Target fields - targetId: targets.targetId, - targetEnabled: targets.enabled, - ip: targets.ip, - 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, - siteType: sites.type, - siteOnline: sites.online, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }) - .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), - 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 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, 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(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, - proxyPort: row.proxyPort, - protocol: row.protocol, - subdomain: row.subdomain, - domainId: row.domainId, - enabled: row.enabled, - stickySession: row.stickySession, - tlsServerName: row.tlsServerName, - setHostHeader: row.setHostHeader, - enableProxy: row.enableProxy, - targets: [], - 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 - rewritePath: row.rewritePath, - rewritePathType: row.rewritePathType, - priority: priority // may be null, we fallback later - }); - } - - // Add target with its associated site data - resourcesMap.get(key).targets.push({ - resourceId: row.resourceId, - targetId: row.targetId, - ip: row.ip, - method: row.method, - port: row.port, - internalPort: row.internalPort, - enabled: row.targetEnabled, - site: { - siteId: row.siteId, - type: row.siteType, - subnet: row.subnet, - exitNodeId: row.exitNodeId, - online: row.siteOnline - } - }); - }); - - // make sure we have at least one resource - if (resourcesMap.size === 0) { - return {}; - } - - const config_output: any = { - http: { - middlewares: { - [redirectHttpsMiddlewareName]: { - redirectScheme: { - scheme: "https" - } - } - } - } - }; - - // get the key and the resource - for (const [key, resource] of resourcesMap.entries()) { - const targets = resource.targets; - - 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`; - - if (!resource.enabled) { - continue; - } - - if (resource.http) { - if (!resource.domainId || !resource.fullDomain) { - continue; - } - - // 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 = {}; - } - - const domainParts = fullDomain.split("."); - let wildCard; - if (domainParts.length <= 2) { - wildCard = `*.${domainParts.join(".")}`; - } else { - wildCard = `*.${domainParts.slice(1).join(".")}`; - } - - if (!resource.subdomain) { - wildCard = resource.fullDomain; - } - - const configDomain = config.getDomain(resource.domainId); - - let certResolver: string, preferWildcardCert: boolean; - if (!configDomain) { - certResolver = config.getRawConfig().traefik.cert_resolver; - preferWildcardCert = - config.getRawConfig().traefik.prefer_wildcard_cert; - } else { - certResolver = configDomain.cert_resolver; - preferWildcardCert = configDomain.prefer_wildcard_cert; - } - - const tls = { - certResolver: certResolver, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; - - const additionalMiddlewares = - config.getRawConfig().traefik.additional_middlewares || []; - - const routerMiddlewares = [ - badgerMiddlewareName, - ...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) { - const headersObj: { [key: string]: string } = {}; - - if (resource.headers) { - let headersArr: { name: string; value: string }[] = []; - try { - headersArr = JSON.parse(resource.headers) as { - name: string; - value: string; - }[]; - } catch (e) { - logger.warn( - `Failed to parse headers for resource ${resource.resourceId}: ${e}` - ); - } - - headersArr.forEach((header) => { - headersObj[header.name] = header.value; - }); - } - - if (resource.setHostHeader) { - headersObj["Host"] = resource.setHostHeader; - } - - if (Object.keys(headersObj).length > 0) { - if (!config_output.http.middlewares) { - config_output.http.middlewares = {}; - } - config_output.http.middlewares[headersMiddlewareName] = { - headers: { - customRequestHeaders: headersObj - } - }; - - routerMiddlewares.push(headersMiddlewareName); - } - } - - // Build routing rules - let rule = `Host(\`${fullDomain}\`)`; - - // 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; - // 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(\`${path}\`)`; - } else if (resource.pathMatchType === "prefix") { - rule += ` && PathPrefix(\`${path}\`)`; - } else if (resource.pathMatchType === "regex") { - rule += ` && PathRegexp(\`${resource.path}\`)`; // this is the raw path because it's a regex - } - } - - config_output.http.routers![routerName] = { - entryPoints: [ - resource.ssl - ? config.getRawConfig().traefik.https_entrypoint - : config.getRawConfig().traefik.http_entrypoint - ], - middlewares: routerMiddlewares, - service: serviceName, - rule: rule, - priority: priority, - ...(resource.ssl ? { tls } : {}) - }; - - if (resource.ssl) { - config_output.http.routers![routerName + "-redirect"] = { - entryPoints: [ - config.getRawConfig().traefik.http_entrypoint - ], - middlewares: [redirectHttpsMiddlewareName], - service: serviceName, - rule: rule, - priority: priority - }; - } - - config_output.http.services![serviceName] = { - loadBalancer: { - servers: (() => { - // Check if any sites are online - // THIS IS SO THAT THERE IS SOME IMMEDIATE FEEDBACK - // EVEN IF THE SITES HAVE NOT UPDATED YET FROM THE - // RECEIVE BANDWIDTH ENDPOINT. - - // TODO: HOW TO HANDLE ^^^^^^ BETTER - const anySitesOnline = ( - targets as TargetWithSite[] - ).some((target: TargetWithSite) => target.site.online); - - return ( - (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - - // If any sites are online, exclude offline sites - if (anySitesOnline && !target.site.online) { - return false; - } - - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if ( - !target.ip || - !target.port || - !target.method - ) { - return false; - } - } else if (target.site.type === "newt") { - if ( - !target.internalPort || - !target.method || - !target.site.subnet - ) { - return false; - } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = - target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }) - // filter out duplicates - .filter( - (v, i, a) => - a.findIndex( - (t) => t && v && t.url === v.url - ) === i - ) - ); - })(), - ...(resource.stickySession - ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } - : {}) - } - }; - - // Add the serversTransport if TLS server name is provided - if (resource.tlsServerName) { - if (!config_output.http.serversTransports) { - config_output.http.serversTransports = {}; - } - config_output.http.serversTransports![transportName] = { - serverName: resource.tlsServerName, - //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings - // if defined in the static config and here. if not set, self-signed certs won't work - insecureSkipVerify: true - }; - config_output.http.services![ - serviceName - ].loadBalancer.serversTransport = transportName; - } - } else { - // Non-HTTP (TCP/UDP) configuration - if (!resource.enableProxy || !resource.proxyPort) { - continue; - } - - const protocol = resource.protocol.toLowerCase(); - const port = resource.proxyPort; - - if (!port) { - continue; - } - - if (!config_output[protocol]) { - config_output[protocol] = { - routers: {}, - services: {} - }; - } - - config_output[protocol].routers[routerName] = { - entryPoints: [`${protocol}-${port}`], - service: serviceName, - ...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {}) - }; - - config_output[protocol].services[serviceName] = { - loadBalancer: { - servers: (() => { - // Check if any sites are online - const anySitesOnline = ( - targets as TargetWithSite[] - ).some((target: TargetWithSite) => target.site.online); - - return (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } - - // If any sites are online, exclude offline sites - if (anySitesOnline && !target.site.online) { - return false; - } - - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - if (!target.ip || !target.port) { - return false; - } - } else if (target.site.type === "newt") { - if ( - !target.internalPort || - !target.site.subnet - ) { - return false; - } - } - return true; - }) - .map((target: TargetWithSite) => { - if ( - target.site.type === "local" || - target.site.type === "wireguard" - ) { - return { - address: `${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { - const ip = - target.site.subnet!.split("/")[0]; - return { - address: `${ip}:${target.internalPort}` - }; - } - }); - })(), - ...(resource.stickySession - ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } - : {}) - } - }; - } - } - return config_output; -} diff --git a/server/lib/traefik/index.ts b/server/lib/traefik/index.ts deleted file mode 100644 index 5630028c..00000000 --- a/server/lib/traefik/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./getTraefikConfig"; \ No newline at end of file diff --git a/server/lib/traefik/middleware.ts b/server/lib/traefik/middleware.ts deleted file mode 100644 index e4055976..00000000 --- a/server/lib/traefik/middleware.ts +++ /dev/null @@ -1,140 +0,0 @@ -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/traefik/traefikConfig.test.ts b/server/lib/traefik/traefikConfig.test.ts deleted file mode 100644 index 88e5da49..00000000 --- a/server/lib/traefik/traefikConfig.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { assertEquals } from "@test/assert"; -import { isDomainCoveredByWildcard } from "./TraefikConfigManager"; - -function runTests() { - console.log('Running wildcard domain coverage tests...'); - - // Test case 1: Basic wildcard certificate at example.com - const basicWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: true }] - ]); - - // Should match first-level subdomains - assertEquals( - isDomainCoveredByWildcard('level1.example.com', basicWildcardCerts), - true, - 'Wildcard cert at example.com should match level1.example.com' - ); - - assertEquals( - isDomainCoveredByWildcard('api.example.com', basicWildcardCerts), - true, - 'Wildcard cert at example.com should match api.example.com' - ); - - assertEquals( - isDomainCoveredByWildcard('www.example.com', basicWildcardCerts), - true, - 'Wildcard cert at example.com should match www.example.com' - ); - - // Should match the root domain (exact match) - assertEquals( - isDomainCoveredByWildcard('example.com', basicWildcardCerts), - true, - 'Wildcard cert at example.com should match example.com itself' - ); - - // Should NOT match second-level subdomains - assertEquals( - isDomainCoveredByWildcard('level2.level1.example.com', basicWildcardCerts), - false, - 'Wildcard cert at example.com should NOT match level2.level1.example.com' - ); - - assertEquals( - isDomainCoveredByWildcard('deep.nested.subdomain.example.com', basicWildcardCerts), - false, - 'Wildcard cert at example.com should NOT match deep.nested.subdomain.example.com' - ); - - // Should NOT match different domains - assertEquals( - isDomainCoveredByWildcard('test.otherdomain.com', basicWildcardCerts), - false, - 'Wildcard cert at example.com should NOT match test.otherdomain.com' - ); - - assertEquals( - isDomainCoveredByWildcard('notexample.com', basicWildcardCerts), - false, - 'Wildcard cert at example.com should NOT match notexample.com' - ); - - // Test case 2: Multiple wildcard certificates - const multipleWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: true }], - ['test.org', { exists: true, wildcard: true }], - ['api.service.net', { exists: true, wildcard: true }] - ]); - - assertEquals( - isDomainCoveredByWildcard('app.example.com', multipleWildcardCerts), - true, - 'Should match subdomain of first wildcard cert' - ); - - assertEquals( - isDomainCoveredByWildcard('staging.test.org', multipleWildcardCerts), - true, - 'Should match subdomain of second wildcard cert' - ); - - assertEquals( - isDomainCoveredByWildcard('v1.api.service.net', multipleWildcardCerts), - true, - 'Should match subdomain of third wildcard cert' - ); - - assertEquals( - isDomainCoveredByWildcard('deep.nested.api.service.net', multipleWildcardCerts), - false, - 'Should NOT match multi-level subdomain of third wildcard cert' - ); - - // Test exact domain matches for multiple certs - assertEquals( - isDomainCoveredByWildcard('example.com', multipleWildcardCerts), - true, - 'Should match exact domain of first wildcard cert' - ); - - assertEquals( - isDomainCoveredByWildcard('test.org', multipleWildcardCerts), - true, - 'Should match exact domain of second wildcard cert' - ); - - assertEquals( - isDomainCoveredByWildcard('api.service.net', multipleWildcardCerts), - true, - 'Should match exact domain of third wildcard cert' - ); - - // Test case 3: Non-wildcard certificates (should not match anything) - const nonWildcardCerts = new Map([ - ['example.com', { exists: true, wildcard: false }], - ['specific.domain.com', { exists: true, wildcard: false }] - ]); - - assertEquals( - isDomainCoveredByWildcard('sub.example.com', nonWildcardCerts), - false, - 'Non-wildcard cert should not match subdomains' - ); - - assertEquals( - isDomainCoveredByWildcard('example.com', nonWildcardCerts), - false, - 'Non-wildcard cert should not match even exact domain via this function' - ); - - // Test case 4: Non-existent certificates (should not match) - const nonExistentCerts = new Map([ - ['example.com', { exists: false, wildcard: true }], - ['missing.com', { exists: false, wildcard: true }] - ]); - - assertEquals( - isDomainCoveredByWildcard('sub.example.com', nonExistentCerts), - false, - 'Non-existent wildcard cert should not match' - ); - - // Test case 5: Edge cases with special domain names - const specialDomainCerts = new Map([ - ['localhost', { exists: true, wildcard: true }], - ['127-0-0-1.nip.io', { exists: true, wildcard: true }], - ['xn--e1afmkfd.xn--p1ai', { exists: true, wildcard: true }] // IDN domain - ]); - - assertEquals( - isDomainCoveredByWildcard('app.localhost', specialDomainCerts), - true, - 'Should match subdomain of localhost wildcard' - ); - - assertEquals( - isDomainCoveredByWildcard('test.127-0-0-1.nip.io', specialDomainCerts), - true, - 'Should match subdomain of nip.io wildcard' - ); - - assertEquals( - isDomainCoveredByWildcard('sub.xn--e1afmkfd.xn--p1ai', specialDomainCerts), - true, - 'Should match subdomain of IDN wildcard' - ); - - // Test case 6: Empty input and edge cases - const emptyCerts = new Map(); - - assertEquals( - isDomainCoveredByWildcard('any.domain.com', emptyCerts), - false, - 'Empty certificate map should not match any domain' - ); - - // Test case 7: Domains with single character components - const singleCharCerts = new Map([ - ['a.com', { exists: true, wildcard: true }], - ['x.y.z', { exists: true, wildcard: true }] - ]); - - assertEquals( - isDomainCoveredByWildcard('b.a.com', singleCharCerts), - true, - 'Should match single character subdomain' - ); - - assertEquals( - isDomainCoveredByWildcard('w.x.y.z', singleCharCerts), - true, - 'Should match single character subdomain of multi-part domain' - ); - - assertEquals( - isDomainCoveredByWildcard('v.w.x.y.z', singleCharCerts), - false, - 'Should NOT match multi-level subdomain of single char domain' - ); - - // Test case 8: Domains with numbers and hyphens - const numericCerts = new Map([ - ['api-v2.service-1.com', { exists: true, wildcard: true }], - ['123.456.net', { exists: true, wildcard: true }] - ]); - - assertEquals( - isDomainCoveredByWildcard('staging.api-v2.service-1.com', numericCerts), - true, - 'Should match subdomain with hyphens and numbers' - ); - - assertEquals( - isDomainCoveredByWildcard('test.123.456.net', numericCerts), - true, - 'Should match subdomain with numeric components' - ); - - assertEquals( - isDomainCoveredByWildcard('deep.staging.api-v2.service-1.com', numericCerts), - false, - 'Should NOT match multi-level subdomain with hyphens and numbers' - ); - - console.log('All wildcard domain coverage tests passed!'); -} - -// Run all tests -try { - runTests(); -} catch (error) { - console.error('Test failed:', error); - process.exit(1); -} diff --git a/server/lib/traefik/utils.ts b/server/lib/traefik/utils.ts deleted file mode 100644 index 37ebfa0b..00000000 --- a/server/lib/traefik/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -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 59776105..e33c9181 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -9,10 +9,6 @@ export function isValidIP(ip: string): boolean { } export function isValidUrlGlobPattern(pattern: string): boolean { - if (pattern === "/") { - return true; - } - // Remove leading slash if present pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; @@ -93,1536 +89,3 @@ export function isTargetValid(value: string | undefined) { return DOMAIN_REGEX.test(value); } - -export function isValidDomain(domain: string): boolean { - // Check overall length - if (domain.length > 253) return false; - - // Check for invalid characters or patterns - if ( - domain.startsWith(".") || - domain.endsWith(".") || - domain.includes("..") - ) { - return false; - } - - const labels = domain.split("."); - - // Must have at least 2 labels (domain + TLD) - if (labels.length < 2) return false; - - // Validate each label - for (const label of labels) { - if (label.length === 0 || label.length > 63) return false; - if (label.startsWith("-") || label.endsWith("-")) return false; - if (!/^[a-zA-Z0-9-]+$/.test(label)) return false; - } - - // TLD should be at least 2 characters and contain only letters - const tld = labels[labels.length - 1]; - if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) return false; - - // Check if TLD is in the list of valid TLDs - if (!validTlds.includes(tld.toUpperCase())) return false; - - return true; -} - -export function validateHeaders(headers: string): boolean { - // Validate comma-separated headers in format "Header-Name: value" - const headerPairs = headers.split(",").map((pair) => pair.trim()); - return headerPairs.every((pair) => { - // Check if the pair contains exactly one colon - const colonCount = (pair.match(/:/g) || []).length; - if (colonCount !== 1) { - return false; - } - - const colonIndex = pair.indexOf(":"); - if (colonIndex === 0 || colonIndex === pair.length - 1) { - return false; - } - - const headerName = pair.substring(0, colonIndex).trim(); - const headerValue = pair.substring(colonIndex + 1).trim(); - - // Header name should not be empty and should contain valid characters - // Header names are case-insensitive and can contain alphanumeric, hyphens - const headerNameRegex = /^[a-zA-Z0-9\-_]+$/; - if (!headerName || !headerNameRegex.test(headerName)) { - return false; - } - - // Header value should not be empty and should not contain colons - if (!headerValue || headerValue.includes(":")) { - return false; - } - - return true; - }); -} - -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", - "ABB", - "ABBOTT", - "ABBVIE", - "ABC", - "ABLE", - "ABOGADO", - "ABUDHABI", - "AC", - "ACADEMY", - "ACCENTURE", - "ACCOUNTANT", - "ACCOUNTANTS", - "ACO", - "ACTOR", - "AD", - "ADS", - "ADULT", - "AE", - "AEG", - "AERO", - "AETNA", - "AF", - "AFL", - "AFRICA", - "AG", - "AGAKHAN", - "AGENCY", - "AI", - "AIG", - "AIRBUS", - "AIRFORCE", - "AIRTEL", - "AKDN", - "AL", - "ALIBABA", - "ALIPAY", - "ALLFINANZ", - "ALLSTATE", - "ALLY", - "ALSACE", - "ALSTOM", - "AM", - "AMAZON", - "AMERICANEXPRESS", - "AMERICANFAMILY", - "AMEX", - "AMFAM", - "AMICA", - "AMSTERDAM", - "ANALYTICS", - "ANDROID", - "ANQUAN", - "ANZ", - "AO", - "AOL", - "APARTMENTS", - "APP", - "APPLE", - "AQ", - "AQUARELLE", - "AR", - "ARAB", - "ARAMCO", - "ARCHI", - "ARMY", - "ARPA", - "ART", - "ARTE", - "AS", - "ASDA", - "ASIA", - "ASSOCIATES", - "AT", - "ATHLETA", - "ATTORNEY", - "AU", - "AUCTION", - "AUDI", - "AUDIBLE", - "AUDIO", - "AUSPOST", - "AUTHOR", - "AUTO", - "AUTOS", - "AW", - "AWS", - "AX", - "AXA", - "AZ", - "AZURE", - "BA", - "BABY", - "BAIDU", - "BANAMEX", - "BAND", - "BANK", - "BAR", - "BARCELONA", - "BARCLAYCARD", - "BARCLAYS", - "BAREFOOT", - "BARGAINS", - "BASEBALL", - "BASKETBALL", - "BAUHAUS", - "BAYERN", - "BB", - "BBC", - "BBT", - "BBVA", - "BCG", - "BCN", - "BD", - "BE", - "BEATS", - "BEAUTY", - "BEER", - "BERLIN", - "BEST", - "BESTBUY", - "BET", - "BF", - "BG", - "BH", - "BHARTI", - "BI", - "BIBLE", - "BID", - "BIKE", - "BING", - "BINGO", - "BIO", - "BIZ", - "BJ", - "BLACK", - "BLACKFRIDAY", - "BLOCKBUSTER", - "BLOG", - "BLOOMBERG", - "BLUE", - "BM", - "BMS", - "BMW", - "BN", - "BNPPARIBAS", - "BO", - "BOATS", - "BOEHRINGER", - "BOFA", - "BOM", - "BOND", - "BOO", - "BOOK", - "BOOKING", - "BOSCH", - "BOSTIK", - "BOSTON", - "BOT", - "BOUTIQUE", - "BOX", - "BR", - "BRADESCO", - "BRIDGESTONE", - "BROADWAY", - "BROKER", - "BROTHER", - "BRUSSELS", - "BS", - "BT", - "BUILD", - "BUILDERS", - "BUSINESS", - "BUY", - "BUZZ", - "BV", - "BW", - "BY", - "BZ", - "BZH", - "CA", - "CAB", - "CAFE", - "CAL", - "CALL", - "CALVINKLEIN", - "CAM", - "CAMERA", - "CAMP", - "CANON", - "CAPETOWN", - "CAPITAL", - "CAPITALONE", - "CAR", - "CARAVAN", - "CARDS", - "CARE", - "CAREER", - "CAREERS", - "CARS", - "CASA", - "CASE", - "CASH", - "CASINO", - "CAT", - "CATERING", - "CATHOLIC", - "CBA", - "CBN", - "CBRE", - "CC", - "CD", - "CENTER", - "CEO", - "CERN", - "CF", - "CFA", - "CFD", - "CG", - "CH", - "CHANEL", - "CHANNEL", - "CHARITY", - "CHASE", - "CHAT", - "CHEAP", - "CHINTAI", - "CHRISTMAS", - "CHROME", - "CHURCH", - "CI", - "CIPRIANI", - "CIRCLE", - "CISCO", - "CITADEL", - "CITI", - "CITIC", - "CITY", - "CK", - "CL", - "CLAIMS", - "CLEANING", - "CLICK", - "CLINIC", - "CLINIQUE", - "CLOTHING", - "CLOUD", - "CLUB", - "CLUBMED", - "CM", - "CN", - "CO", - "COACH", - "CODES", - "COFFEE", - "COLLEGE", - "COLOGNE", - "COM", - "COMMBANK", - "COMMUNITY", - "COMPANY", - "COMPARE", - "COMPUTER", - "COMSEC", - "CONDOS", - "CONSTRUCTION", - "CONSULTING", - "CONTACT", - "CONTRACTORS", - "COOKING", - "COOL", - "COOP", - "CORSICA", - "COUNTRY", - "COUPON", - "COUPONS", - "COURSES", - "CPA", - "CR", - "CREDIT", - "CREDITCARD", - "CREDITUNION", - "CRICKET", - "CROWN", - "CRS", - "CRUISE", - "CRUISES", - "CU", - "CUISINELLA", - "CV", - "CW", - "CX", - "CY", - "CYMRU", - "CYOU", - "CZ", - "DAD", - "DANCE", - "DATA", - "DATE", - "DATING", - "DATSUN", - "DAY", - "DCLK", - "DDS", - "DE", - "DEAL", - "DEALER", - "DEALS", - "DEGREE", - "DELIVERY", - "DELL", - "DELOITTE", - "DELTA", - "DEMOCRAT", - "DENTAL", - "DENTIST", - "DESI", - "DESIGN", - "DEV", - "DHL", - "DIAMONDS", - "DIET", - "DIGITAL", - "DIRECT", - "DIRECTORY", - "DISCOUNT", - "DISCOVER", - "DISH", - "DIY", - "DJ", - "DK", - "DM", - "DNP", - "DO", - "DOCS", - "DOCTOR", - "DOG", - "DOMAINS", - "DOT", - "DOWNLOAD", - "DRIVE", - "DTV", - "DUBAI", - "DUNLOP", - "DUPONT", - "DURBAN", - "DVAG", - "DVR", - "DZ", - "EARTH", - "EAT", - "EC", - "ECO", - "EDEKA", - "EDU", - "EDUCATION", - "EE", - "EG", - "EMAIL", - "EMERCK", - "ENERGY", - "ENGINEER", - "ENGINEERING", - "ENTERPRISES", - "EPSON", - "EQUIPMENT", - "ER", - "ERICSSON", - "ERNI", - "ES", - "ESQ", - "ESTATE", - "ET", - "EU", - "EUROVISION", - "EUS", - "EVENTS", - "EXCHANGE", - "EXPERT", - "EXPOSED", - "EXPRESS", - "EXTRASPACE", - "FAGE", - "FAIL", - "FAIRWINDS", - "FAITH", - "FAMILY", - "FAN", - "FANS", - "FARM", - "FARMERS", - "FASHION", - "FAST", - "FEDEX", - "FEEDBACK", - "FERRARI", - "FERRERO", - "FI", - "FIDELITY", - "FIDO", - "FILM", - "FINAL", - "FINANCE", - "FINANCIAL", - "FIRE", - "FIRESTONE", - "FIRMDALE", - "FISH", - "FISHING", - "FIT", - "FITNESS", - "FJ", - "FK", - "FLICKR", - "FLIGHTS", - "FLIR", - "FLORIST", - "FLOWERS", - "FLY", - "FM", - "FO", - "FOO", - "FOOD", - "FOOTBALL", - "FORD", - "FOREX", - "FORSALE", - "FORUM", - "FOUNDATION", - "FOX", - "FR", - "FREE", - "FRESENIUS", - "FRL", - "FROGANS", - "FRONTIER", - "FTR", - "FUJITSU", - "FUN", - "FUND", - "FURNITURE", - "FUTBOL", - "FYI", - "GA", - "GAL", - "GALLERY", - "GALLO", - "GALLUP", - "GAME", - "GAMES", - "GAP", - "GARDEN", - "GAY", - "GB", - "GBIZ", - "GD", - "GDN", - "GE", - "GEA", - "GENT", - "GENTING", - "GEORGE", - "GF", - "GG", - "GGEE", - "GH", - "GI", - "GIFT", - "GIFTS", - "GIVES", - "GIVING", - "GL", - "GLASS", - "GLE", - "GLOBAL", - "GLOBO", - "GM", - "GMAIL", - "GMBH", - "GMO", - "GMX", - "GN", - "GODADDY", - "GOLD", - "GOLDPOINT", - "GOLF", - "GOO", - "GOODYEAR", - "GOOG", - "GOOGLE", - "GOP", - "GOT", - "GOV", - "GP", - "GQ", - "GR", - "GRAINGER", - "GRAPHICS", - "GRATIS", - "GREEN", - "GRIPE", - "GROCERY", - "GROUP", - "GS", - "GT", - "GU", - "GUCCI", - "GUGE", - "GUIDE", - "GUITARS", - "GURU", - "GW", - "GY", - "HAIR", - "HAMBURG", - "HANGOUT", - "HAUS", - "HBO", - "HDFC", - "HDFCBANK", - "HEALTH", - "HEALTHCARE", - "HELP", - "HELSINKI", - "HERE", - "HERMES", - "HIPHOP", - "HISAMITSU", - "HITACHI", - "HIV", - "HK", - "HKT", - "HM", - "HN", - "HOCKEY", - "HOLDINGS", - "HOLIDAY", - "HOMEDEPOT", - "HOMEGOODS", - "HOMES", - "HOMESENSE", - "HONDA", - "HORSE", - "HOSPITAL", - "HOST", - "HOSTING", - "HOT", - "HOTELS", - "HOTMAIL", - "HOUSE", - "HOW", - "HR", - "HSBC", - "HT", - "HU", - "HUGHES", - "HYATT", - "HYUNDAI", - "IBM", - "ICBC", - "ICE", - "ICU", - "ID", - "IE", - "IEEE", - "IFM", - "IKANO", - "IL", - "IM", - "IMAMAT", - "IMDB", - "IMMO", - "IMMOBILIEN", - "IN", - "INC", - "INDUSTRIES", - "INFINITI", - "INFO", - "ING", - "INK", - "INSTITUTE", - "INSURANCE", - "INSURE", - "INT", - "INTERNATIONAL", - "INTUIT", - "INVESTMENTS", - "IO", - "IPIRANGA", - "IQ", - "IR", - "IRISH", - "IS", - "ISMAILI", - "IST", - "ISTANBUL", - "IT", - "ITAU", - "ITV", - "JAGUAR", - "JAVA", - "JCB", - "JE", - "JEEP", - "JETZT", - "JEWELRY", - "JIO", - "JLL", - "JM", - "JMP", - "JNJ", - "JO", - "JOBS", - "JOBURG", - "JOT", - "JOY", - "JP", - "JPMORGAN", - "JPRS", - "JUEGOS", - "JUNIPER", - "KAUFEN", - "KDDI", - "KE", - "KERRYHOTELS", - "KERRYPROPERTIES", - "KFH", - "KG", - "KH", - "KI", - "KIA", - "KIDS", - "KIM", - "KINDLE", - "KITCHEN", - "KIWI", - "KM", - "KN", - "KOELN", - "KOMATSU", - "KOSHER", - "KP", - "KPMG", - "KPN", - "KR", - "KRD", - "KRED", - "KUOKGROUP", - "KW", - "KY", - "KYOTO", - "KZ", - "LA", - "LACAIXA", - "LAMBORGHINI", - "LAMER", - "LAND", - "LANDROVER", - "LANXESS", - "LASALLE", - "LAT", - "LATINO", - "LATROBE", - "LAW", - "LAWYER", - "LB", - "LC", - "LDS", - "LEASE", - "LECLERC", - "LEFRAK", - "LEGAL", - "LEGO", - "LEXUS", - "LGBT", - "LI", - "LIDL", - "LIFE", - "LIFEINSURANCE", - "LIFESTYLE", - "LIGHTING", - "LIKE", - "LILLY", - "LIMITED", - "LIMO", - "LINCOLN", - "LINK", - "LIVE", - "LIVING", - "LK", - "LLC", - "LLP", - "LOAN", - "LOANS", - "LOCKER", - "LOCUS", - "LOL", - "LONDON", - "LOTTE", - "LOTTO", - "LOVE", - "LPL", - "LPLFINANCIAL", - "LR", - "LS", - "LT", - "LTD", - "LTDA", - "LU", - "LUNDBECK", - "LUXE", - "LUXURY", - "LV", - "LY", - "MA", - "MADRID", - "MAIF", - "MAISON", - "MAKEUP", - "MAN", - "MANAGEMENT", - "MANGO", - "MAP", - "MARKET", - "MARKETING", - "MARKETS", - "MARRIOTT", - "MARSHALLS", - "MATTEL", - "MBA", - "MC", - "MCKINSEY", - "MD", - "ME", - "MED", - "MEDIA", - "MEET", - "MELBOURNE", - "MEME", - "MEMORIAL", - "MEN", - "MENU", - "MERCKMSD", - "MG", - "MH", - "MIAMI", - "MICROSOFT", - "MIL", - "MINI", - "MINT", - "MIT", - "MITSUBISHI", - "MK", - "ML", - "MLB", - "MLS", - "MM", - "MMA", - "MN", - "MO", - "MOBI", - "MOBILE", - "MODA", - "MOE", - "MOI", - "MOM", - "MONASH", - "MONEY", - "MONSTER", - "MORMON", - "MORTGAGE", - "MOSCOW", - "MOTO", - "MOTORCYCLES", - "MOV", - "MOVIE", - "MP", - "MQ", - "MR", - "MS", - "MSD", - "MT", - "MTN", - "MTR", - "MU", - "MUSEUM", - "MUSIC", - "MV", - "MW", - "MX", - "MY", - "MZ", - "NA", - "NAB", - "NAGOYA", - "NAME", - "NAVY", - "NBA", - "NC", - "NE", - "NEC", - "NET", - "NETBANK", - "NETFLIX", - "NETWORK", - "NEUSTAR", - "NEW", - "NEWS", - "NEXT", - "NEXTDIRECT", - "NEXUS", - "NF", - "NFL", - "NG", - "NGO", - "NHK", - "NI", - "NICO", - "NIKE", - "NIKON", - "NINJA", - "NISSAN", - "NISSAY", - "NL", - "NO", - "NOKIA", - "NORTON", - "NOW", - "NOWRUZ", - "NOWTV", - "NP", - "NR", - "NRA", - "NRW", - "NTT", - "NU", - "NYC", - "NZ", - "OBI", - "OBSERVER", - "OFFICE", - "OKINAWA", - "OLAYAN", - "OLAYANGROUP", - "OLLO", - "OM", - "OMEGA", - "ONE", - "ONG", - "ONL", - "ONLINE", - "OOO", - "OPEN", - "ORACLE", - "ORANGE", - "ORG", - "ORGANIC", - "ORIGINS", - "OSAKA", - "OTSUKA", - "OTT", - "OVH", - "PA", - "PAGE", - "PANASONIC", - "PARIS", - "PARS", - "PARTNERS", - "PARTS", - "PARTY", - "PAY", - "PCCW", - "PE", - "PET", - "PF", - "PFIZER", - "PG", - "PH", - "PHARMACY", - "PHD", - "PHILIPS", - "PHONE", - "PHOTO", - "PHOTOGRAPHY", - "PHOTOS", - "PHYSIO", - "PICS", - "PICTET", - "PICTURES", - "PID", - "PIN", - "PING", - "PINK", - "PIONEER", - "PIZZA", - "PK", - "PL", - "PLACE", - "PLAY", - "PLAYSTATION", - "PLUMBING", - "PLUS", - "PM", - "PN", - "PNC", - "POHL", - "POKER", - "POLITIE", - "PORN", - "POST", - "PR", - "PRAXI", - "PRESS", - "PRIME", - "PRO", - "PROD", - "PRODUCTIONS", - "PROF", - "PROGRESSIVE", - "PROMO", - "PROPERTIES", - "PROPERTY", - "PROTECTION", - "PRU", - "PRUDENTIAL", - "PS", - "PT", - "PUB", - "PW", - "PWC", - "PY", - "QA", - "QPON", - "QUEBEC", - "QUEST", - "RACING", - "RADIO", - "RE", - "READ", - "REALESTATE", - "REALTOR", - "REALTY", - "RECIPES", - "RED", - "REDSTONE", - "REDUMBRELLA", - "REHAB", - "REISE", - "REISEN", - "REIT", - "RELIANCE", - "REN", - "RENT", - "RENTALS", - "REPAIR", - "REPORT", - "REPUBLICAN", - "REST", - "RESTAURANT", - "REVIEW", - "REVIEWS", - "REXROTH", - "RICH", - "RICHARDLI", - "RICOH", - "RIL", - "RIO", - "RIP", - "RO", - "ROCKS", - "RODEO", - "ROGERS", - "ROOM", - "RS", - "RSVP", - "RU", - "RUGBY", - "RUHR", - "RUN", - "RW", - "RWE", - "RYUKYU", - "SA", - "SAARLAND", - "SAFE", - "SAFETY", - "SAKURA", - "SALE", - "SALON", - "SAMSCLUB", - "SAMSUNG", - "SANDVIK", - "SANDVIKCOROMANT", - "SANOFI", - "SAP", - "SARL", - "SAS", - "SAVE", - "SAXO", - "SB", - "SBI", - "SBS", - "SC", - "SCB", - "SCHAEFFLER", - "SCHMIDT", - "SCHOLARSHIPS", - "SCHOOL", - "SCHULE", - "SCHWARZ", - "SCIENCE", - "SCOT", - "SD", - "SE", - "SEARCH", - "SEAT", - "SECURE", - "SECURITY", - "SEEK", - "SELECT", - "SENER", - "SERVICES", - "SEVEN", - "SEW", - "SEX", - "SEXY", - "SFR", - "SG", - "SH", - "SHANGRILA", - "SHARP", - "SHELL", - "SHIA", - "SHIKSHA", - "SHOES", - "SHOP", - "SHOPPING", - "SHOUJI", - "SHOW", - "SI", - "SILK", - "SINA", - "SINGLES", - "SITE", - "SJ", - "SK", - "SKI", - "SKIN", - "SKY", - "SKYPE", - "SL", - "SLING", - "SM", - "SMART", - "SMILE", - "SN", - "SNCF", - "SO", - "SOCCER", - "SOCIAL", - "SOFTBANK", - "SOFTWARE", - "SOHU", - "SOLAR", - "SOLUTIONS", - "SONG", - "SONY", - "SOY", - "SPA", - "SPACE", - "SPORT", - "SPOT", - "SR", - "SRL", - "SS", - "ST", - "STADA", - "STAPLES", - "STAR", - "STATEBANK", - "STATEFARM", - "STC", - "STCGROUP", - "STOCKHOLM", - "STORAGE", - "STORE", - "STREAM", - "STUDIO", - "STUDY", - "STYLE", - "SU", - "SUCKS", - "SUPPLIES", - "SUPPLY", - "SUPPORT", - "SURF", - "SURGERY", - "SUZUKI", - "SV", - "SWATCH", - "SWISS", - "SX", - "SY", - "SYDNEY", - "SYSTEMS", - "SZ", - "TAB", - "TAIPEI", - "TALK", - "TAOBAO", - "TARGET", - "TATAMOTORS", - "TATAR", - "TATTOO", - "TAX", - "TAXI", - "TC", - "TCI", - "TD", - "TDK", - "TEAM", - "TECH", - "TECHNOLOGY", - "TEL", - "TEMASEK", - "TENNIS", - "TEVA", - "TF", - "TG", - "TH", - "THD", - "THEATER", - "THEATRE", - "TIAA", - "TICKETS", - "TIENDA", - "TIPS", - "TIRES", - "TIROL", - "TJ", - "TJMAXX", - "TJX", - "TK", - "TKMAXX", - "TL", - "TM", - "TMALL", - "TN", - "TO", - "TODAY", - "TOKYO", - "TOOLS", - "TOP", - "TORAY", - "TOSHIBA", - "TOTAL", - "TOURS", - "TOWN", - "TOYOTA", - "TOYS", - "TR", - "TRADE", - "TRADING", - "TRAINING", - "TRAVEL", - "TRAVELERS", - "TRAVELERSINSURANCE", - "TRUST", - "TRV", - "TT", - "TUBE", - "TUI", - "TUNES", - "TUSHU", - "TV", - "TVS", - "TW", - "TZ", - "UA", - "UBANK", - "UBS", - "UG", - "UK", - "UNICOM", - "UNIVERSITY", - "UNO", - "UOL", - "UPS", - "US", - "UY", - "UZ", - "VA", - "VACATIONS", - "VANA", - "VANGUARD", - "VC", - "VE", - "VEGAS", - "VENTURES", - "VERISIGN", - "VERSICHERUNG", - "VET", - "VG", - "VI", - "VIAJES", - "VIDEO", - "VIG", - "VIKING", - "VILLAS", - "VIN", - "VIP", - "VIRGIN", - "VISA", - "VISION", - "VIVA", - "VIVO", - "VLAANDEREN", - "VN", - "VODKA", - "VOLVO", - "VOTE", - "VOTING", - "VOTO", - "VOYAGE", - "VU", - "WALES", - "WALMART", - "WALTER", - "WANG", - "WANGGOU", - "WATCH", - "WATCHES", - "WEATHER", - "WEATHERCHANNEL", - "WEBCAM", - "WEBER", - "WEBSITE", - "WED", - "WEDDING", - "WEIBO", - "WEIR", - "WF", - "WHOSWHO", - "WIEN", - "WIKI", - "WILLIAMHILL", - "WIN", - "WINDOWS", - "WINE", - "WINNERS", - "WME", - "WOLTERSKLUWER", - "WOODSIDE", - "WORK", - "WORKS", - "WORLD", - "WOW", - "WS", - "WTC", - "WTF", - "XBOX", - "XEROX", - "XIHUAN", - "XIN", - "XN--11B4C3D", - "XN--1CK2E1B", - "XN--1QQW23A", - "XN--2SCRJ9C", - "XN--30RR7Y", - "XN--3BST00M", - "XN--3DS443G", - "XN--3E0B707E", - "XN--3HCRJ9C", - "XN--3PXU8K", - "XN--42C2D9A", - "XN--45BR5CYL", - "XN--45BRJ9C", - "XN--45Q11C", - "XN--4DBRK0CE", - "XN--4GBRIM", - "XN--54B7FTA0CC", - "XN--55QW42G", - "XN--55QX5D", - "XN--5SU34J936BGSG", - "XN--5TZM5G", - "XN--6FRZ82G", - "XN--6QQ986B3XL", - "XN--80ADXHKS", - "XN--80AO21A", - "XN--80AQECDR1A", - "XN--80ASEHDB", - "XN--80ASWG", - "XN--8Y0A063A", - "XN--90A3AC", - "XN--90AE", - "XN--90AIS", - "XN--9DBQ2A", - "XN--9ET52U", - "XN--9KRT00A", - "XN--B4W605FERD", - "XN--BCK1B9A5DRE4C", - "XN--C1AVG", - "XN--C2BR7G", - "XN--CCK2B3B", - "XN--CCKWCXETD", - "XN--CG4BKI", - "XN--CLCHC0EA0B2G2A9GCD", - "XN--CZR694B", - "XN--CZRS0T", - "XN--CZRU2D", - "XN--D1ACJ3B", - "XN--D1ALF", - "XN--E1A4C", - "XN--ECKVDTC9D", - "XN--EFVY88H", - "XN--FCT429K", - "XN--FHBEI", - "XN--FIQ228C5HS", - "XN--FIQ64B", - "XN--FIQS8S", - "XN--FIQZ9S", - "XN--FJQ720A", - "XN--FLW351E", - "XN--FPCRJ9C3D", - "XN--FZC2C9E2C", - "XN--FZYS8D69UVGM", - "XN--G2XX48C", - "XN--GCKR3F0F", - "XN--GECRJ9C", - "XN--GK3AT1E", - "XN--H2BREG3EVE", - "XN--H2BRJ9C", - "XN--H2BRJ9C8C", - "XN--HXT814E", - "XN--I1B6B1A6A2E", - "XN--IMR513N", - "XN--IO0A7I", - "XN--J1AEF", - "XN--J1AMH", - "XN--J6W193G", - "XN--JLQ480N2RG", - "XN--JVR189M", - "XN--KCRX77D1X4A", - "XN--KPRW13D", - "XN--KPRY57D", - "XN--KPUT3I", - "XN--L1ACC", - "XN--LGBBAT1AD8J", - "XN--MGB9AWBF", - "XN--MGBA3A3EJT", - "XN--MGBA3A4F16A", - "XN--MGBA7C0BBN0A", - "XN--MGBAAM7A8H", - "XN--MGBAB2BD", - "XN--MGBAH1A3HJKRD", - "XN--MGBAI9AZGQP6J", - "XN--MGBAYH7GPA", - "XN--MGBBH1A", - "XN--MGBBH1A71E", - "XN--MGBC0A9AZCG", - "XN--MGBCA7DZDO", - "XN--MGBCPQ6GPA1A", - "XN--MGBERP4A5D4AR", - "XN--MGBGU82A", - "XN--MGBI4ECEXP", - "XN--MGBPL2FH", - "XN--MGBT3DHD", - "XN--MGBTX2B", - "XN--MGBX4CD0AB", - "XN--MIX891F", - "XN--MK1BU44C", - "XN--MXTQ1M", - "XN--NGBC5AZD", - "XN--NGBE9E0A", - "XN--NGBRX", - "XN--NODE", - "XN--NQV7F", - "XN--NQV7FS00EMA", - "XN--NYQY26A", - "XN--O3CW4H", - "XN--OGBPF8FL", - "XN--OTU796D", - "XN--P1ACF", - "XN--P1AI", - "XN--PGBS0DH", - "XN--PSSY2U", - "XN--Q7CE6A", - "XN--Q9JYB4C", - "XN--QCKA1PMC", - "XN--QXA6A", - "XN--QXAM", - "XN--RHQV96G", - "XN--ROVU88B", - "XN--RVC1E0AM3E", - "XN--S9BRJ9C", - "XN--SES554G", - "XN--T60B56A", - "XN--TCKWE", - "XN--TIQ49XQYJ", - "XN--UNUP4Y", - "XN--VERMGENSBERATER-CTB", - "XN--VERMGENSBERATUNG-PWB", - "XN--VHQUV", - "XN--VUQ861B", - "XN--W4R85EL8FHU5DNRA", - "XN--W4RS40L", - "XN--WGBH1C", - "XN--WGBL6A", - "XN--XHQ521B", - "XN--XKC2AL3HYE2A", - "XN--XKC2DL3A5EE0H", - "XN--Y9A3AQ", - "XN--YFRO4I67O", - "XN--YGBI2AMMX", - "XN--ZFR164B", - "XXX", - "XYZ", - "YACHTS", - "YAHOO", - "YAMAXUN", - "YANDEX", - "YE", - "YODOBASHI", - "YOGA", - "YOKOHAMA", - "YOU", - "YOUTUBE", - "YT", - "YUN", - "ZA", - "ZAPPOS", - "ZARA", - "ZERO", - "ZIP", - "ZM", - "ZONE", - "ZUERICH", - "ZW", - "" -]; diff --git a/server/logger.ts b/server/logger.ts index 99b0cfbf..cd12d735 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -4,22 +4,6 @@ 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}`; @@ -44,12 +28,7 @@ if (config.getRawConfig().app.save_logs) { maxSize: "20m", maxFiles: "7d", createSymlink: true, - symlinkName: "pangolin.log", - format: winston.format.combine( - winston.format.timestamp({ format: isoLocal }), - winston.format.splat(), - hformat - ) + symlinkName: "pangolin.log" }) ); transports.push( @@ -62,7 +41,7 @@ if (config.getRawConfig().app.save_logs) { createSymlink: true, symlinkName: ".machinelogs.json", format: winston.format.combine( - winston.format.timestamp({ format: isoLocal }), + winston.format.timestamp(), winston.format.splat(), winston.format.json() ) @@ -76,9 +55,7 @@ const logger = winston.createLogger({ winston.format.errors({ stack: true }), winston.format.colorize(), winston.format.splat(), - - // Use isoLocal so timestamps respect TZ env, not just UTC - winston.format.timestamp({ format: isoLocal }), + winston.format.timestamp(), hformat ), transports diff --git a/server/middlewares/getUserOrgs.ts b/server/middlewares/getUserOrgs.ts index 449690f5..7d5c08f7 100644 --- a/server/middlewares/getUserOrgs.ts +++ b/server/middlewares/getUserOrgs.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; -import { db, userOrgs } from "@server/db"; +import { db } from "@server/db"; +import { userOrgs, orgs } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 629cafe9..6dbdcd6f 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -1,4 +1,5 @@ export * from "./notFound"; +export * from "./rateLimit"; export * from "./formatError"; export * from "./verifySession"; export * from "./verifyUser"; @@ -13,17 +14,8 @@ export * from "./verifyAdmin"; export * from "./verifySetResourceUsers"; export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; -export * from "./requestTimeout"; -export * from "./verifyClientAccess"; -export * from "./verifyUserHasAction"; export * from "./verifyUserIsServerAdmin"; export * from "./verifyIsLoggedInUser"; -export * from "./verifyIsLoggedInUser"; -export * from "./verifyClientAccess"; -export * from "./integration"; +// export * from "./integration"; export * from "./verifyUserHasAction"; -export * from "./verifyApiKeyAccess"; -export * from "./verifyDomainAccess"; -export * from "./verifyClientsEnabled"; -export * from "./verifyUserIsOrgOwner"; -export * from "./verifySiteResourceAccess"; +// export * from "./verifyApiKeyAccess"; diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts deleted file mode 100644 index 747cddee..00000000 --- a/server/middlewares/integration/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from "./verifyApiKey"; -export * from "./verifyApiKeyOrgAccess"; -export * from "./verifyApiKeyHasAction"; -export * from "./verifyApiKeySiteAccess"; -export * from "./verifyApiKeyResourceAccess"; -export * from "./verifyApiKeyTargetAccess"; -export * from "./verifyApiKeyRoleAccess"; -export * from "./verifyApiKeyUserAccess"; -export * from "./verifyApiKeySetResourceUsers"; -export * from "./verifyAccessTokenAccess"; -export * from "./verifyApiKeyIsRoot"; -export * from "./verifyApiKeyApiKeyAccess"; -export * from "./verifyApiKeyClientAccess"; -export * from "./verifyApiKeySiteResourceAccess"; \ No newline at end of file diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts deleted file mode 100644 index f5ae8746..00000000 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resourceAccessToken, resources, apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyAccessTokenAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const accessTokenId = req.params.accessTokenId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - const [accessToken] = await db - .select() - .from(resourceAccessToken) - .where(eq(resourceAccessToken.accessTokenId, accessTokenId)) - .limit(1); - - if (!accessToken) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Access token with ID ${accessTokenId} not found` - ) - ); - } - - const resourceId = accessToken.resourceId; - - if (!resourceId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Access token with ID ${accessTokenId} does not have a resource ID` - ) - ); - } - - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - // Verify that the API key is linked to the resource's organization - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying access token access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKey.ts b/server/middlewares/integration/verifyApiKey.ts deleted file mode 100644 index 719b609f..00000000 --- a/server/middlewares/integration/verifyApiKey.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { verifyPassword } from "@server/auth/password"; -import { db } from "@server/db"; -import { apiKeys } from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import { eq } from "drizzle-orm"; -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; - -export async function verifyApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const authHeader = req.headers["authorization"]; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "API key required") - ); - } - - const key = authHeader.split(" ")[1]; // Get the token part after "Bearer" - const [apiKeyId, apiKeySecret] = key.split("."); - - const [apiKey] = await db - .select() - .from(apiKeys) - .where(eq(apiKeys.apiKeyId, apiKeyId)) - .limit(1); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key") - ); - } - - const secretHash = apiKey.apiKeyHash; - const valid = await verifyPassword(apiKeySecret, secretHash); - - if (!valid) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key") - ); - } - - req.apiKey = apiKey; - - return next(); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred checking API key" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts deleted file mode 100644 index ad5b7fc4..00000000 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { apiKeys, apiKeyOrg } from "@server/db"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyApiKeyAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const {apiKey: callerApiKey } = req; - - const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; - - if (!callerApiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") - ); - } - - if (callerApiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - const [callerApiKeyOrg] = await db - .select() - .from(apiKeyOrg) - .where( - and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!callerApiKeyOrg) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `API key with ID ${apiKeyId} does not have an organization ID` - ) - ); - } - - const [otherApiKeyOrg] = await db - .select() - .from(apiKeyOrg) - .where( - and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!otherApiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}` - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying key access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyClientAccess.ts b/server/middlewares/integration/verifyApiKeyClientAccess.ts deleted file mode 100644 index e5ed624d..00000000 --- a/server/middlewares/integration/verifyApiKeyClientAccess.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { clients, db } from "@server/db"; -import { apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyClientAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const clientId = parseInt( - req.params.clientId || req.body.clientId || req.query.clientId - ); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (isNaN(clientId)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID") - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - const client = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (client.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Client with ID ${clientId} not found` - ) - ); - } - - if (!client[0].orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Client with ID ${clientId} does not have an organization ID` - ) - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, client[0].orgId) - ) - ); - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyHasAction.ts b/server/middlewares/integration/verifyApiKeyHasAction.ts deleted file mode 100644 index 428aeed2..00000000 --- a/server/middlewares/integration/verifyApiKeyHasAction.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; -import { ActionsEnum } from "@server/auth/actions"; -import { db } from "@server/db"; -import { apiKeyActions } from "@server/db"; -import { and, eq } from "drizzle-orm"; - -export function verifyApiKeyHasAction(action: ActionsEnum) { - return async function ( - req: Request, - res: Response, - next: NextFunction - ): Promise { - try { - if (!req.apiKey) { - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - "API Key not authenticated" - ) - ); - } - - const [actionRes] = await db - .select() - .from(apiKeyActions) - .where( - and( - eq(apiKeyActions.apiKeyId, req.apiKey.apiKeyId), - eq(apiKeyActions.actionId, action) - ) - ); - - if (!actionRes) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have permission perform this action" - ) - ); - } - - return next(); - } catch (error) { - logger.error("Error verifying key action access:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying key action access" - ) - ); - } - }; -} diff --git a/server/middlewares/integration/verifyApiKeyIsRoot.ts b/server/middlewares/integration/verifyApiKeyIsRoot.ts deleted file mode 100644 index 2ce9c84d..00000000 --- a/server/middlewares/integration/verifyApiKeyIsRoot.ts +++ /dev/null @@ -1,39 +0,0 @@ -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; - -export async function verifyApiKeyIsRoot( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const { apiKey } = req; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!apiKey.isRoot) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have root access" - ) - ); - } - - return next(); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred checking API key" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts deleted file mode 100644 index c705dc0f..00000000 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; - -export async function verifyApiKeyOrgAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKeyId = req.apiKey?.apiKeyId; - const orgId = req.params.orgId; - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (req.apiKey?.isRoot) { - // Root keys can access any key in any org - return next(); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ); - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - if (!req.apiKeyOrg) { - next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (e) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying organization access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts deleted file mode 100644 index 184ee73c..00000000 --- a/server/middlewares/integration/verifyApiKeyResourceAccess.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resources, apiKeyOrg } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyResourceAccess( - req: Request, - res: Response, - next: NextFunction -) { - const apiKey = req.apiKey; - const resourceId = - req.params.resourceId || req.body.resourceId || req.query.resourceId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - try { - // Retrieve the resource - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - // Verify that the API key is linked to the resource's organization - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying resource access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts deleted file mode 100644 index ffe223a6..00000000 --- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { roles, apiKeyOrg } from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; - -export async function verifyApiKeyRoleAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const singleRoleId = parseInt( - req.params.roleId || req.body.roleId || req.query.roleId - ); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - const { roleIds } = req.body; - const allRoleIds = - roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]); - - if (allRoleIds.length === 0) { - return next(); - } - - const rolesData = await db - .select() - .from(roles) - .where(inArray(roles.roleId, allRoleIds)); - - if (rolesData.length !== allRoleIds.length) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "One or more roles not found" - ) - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - const orgIds = new Set(rolesData.map((role) => role.orgId)); - - for (const role of rolesData) { - const apiKeyOrgAccess = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, role.orgId!) - ) - ) - .limit(1); - - if (apiKeyOrgAccess.length === 0) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - `Key does not have access to organization for role ID ${role.roleId}` - ) - ); - } - } - - if (orgIds.size > 1) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Roles must belong to the same organization" - ) - ); - } - - const orgId = orgIds.values().next().value; - - if (!orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Roles do not have an organization ID" - ) - ); - } - - if (!req.apiKeyOrg) { - // Retrieve the API key's organization link if not already set - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ) - .limit(1); - - if (apiKeyOrgRes.length === 0) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - return next(); - } catch (error) { - logger.error("Error verifying role access:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying role access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts deleted file mode 100644 index 51a8f3fc..00000000 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs } from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeySetResourceUsers( - req: Request, - res: Response, - next: NextFunction -) { - const apiKey = req.apiKey; - const userIds = req.body.userIds; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - if (!userIds) { - return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs")); - } - - if (userIds.length === 0) { - return next(); - } - - try { - const orgId = req.apiKeyOrg.orgId; - const userOrgsData = await db - .select() - .from(userOrgs) - .where( - and( - inArray(userOrgs.userId, userIds), - eq(userOrgs.orgId, orgId) - ) - ); - - if (userOrgsData.length !== userIds.length) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to one or more specified users" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error checking if key has access to the specified users" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts deleted file mode 100644 index 0a310d15..00000000 --- a/server/middlewares/integration/verifyApiKeySiteAccess.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { sites, apiKeyOrg } from "@server/db"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeySiteAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const siteId = parseInt( - req.params.siteId || req.body.siteId || req.query.siteId - ); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (isNaN(siteId)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID") - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - const site = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (site.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` - ) - ); - } - - if (!site[0].orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Site with ID ${siteId} does not have an organization ID` - ) - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, site[0].orgId) - ) - ); - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts deleted file mode 100644 index cba94cd1..00000000 --- a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { siteResources, apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeySiteResourceAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const siteResourceId = parseInt(req.params.siteResourceId); - const siteId = parseInt(req.params.siteId); - const orgId = req.params.orgId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!siteResourceId || !siteId || !orgId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Missing required parameters" - ) - ); - } - - if (apiKey.isRoot) { - // Root keys can access any resource in any org - return next(); - } - - // Check if the site resource exists and belongs to the specified site and org - const [siteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); - - if (!siteResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) - ); - } - - // Verify that the API key has access to the organization - if (!req.apiKeyOrg) { - const apiKeyOrgRes = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ) - .limit(1); - - if (apiKeyOrgRes.length === 0) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - req.apiKeyOrg = apiKeyOrgRes[0]; - } - - // Attach the siteResource to the request for use in the next middleware/route - // @ts-ignore - Extending Request type - req.siteResource = siteResource; - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site resource access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts deleted file mode 100644 index 71146c15..00000000 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { resources, targets, apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyTargetAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const targetId = parseInt(req.params.targetId); - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (isNaN(targetId)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID") - ); - } - - const [target] = await db - .select() - .from(targets) - .where(eq(targets.targetId, targetId)) - .limit(1); - - if (!target) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Target with ID ${targetId} not found` - ) - ); - } - - const resourceId = target.resourceId; - if (!resourceId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Target with ID ${targetId} does not have a resource ID` - ) - ); - } - - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - if (!resource.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Resource with ID ${resourceId} does not have an organization ID` - ) - ); - } - - if (!req.apiKeyOrg) { - const apiKeyOrgResult = await db - .select() - .from(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), - eq(apiKeyOrg.orgId, resource.orgId) - ) - ) - .limit(1); - if (apiKeyOrgResult.length > 0) { - req.apiKeyOrg = apiKeyOrgResult[0]; - } - } - - if (!req.apiKeyOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying target access" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts deleted file mode 100644 index a69489bf..00000000 --- a/server/middlewares/integration/verifyApiKeyUserAccess.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyUserAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const apiKey = req.apiKey; - const reqUserId = - req.params.userId || req.body.userId || req.query.userId; - - if (!apiKey) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") - ); - } - - if (!reqUserId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID") - ); - } - - if (apiKey.isRoot) { - // Root keys can access any key in any org - return next(); - } - - if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have organization access" - ) - ); - } - - const orgId = req.apiKeyOrg.orgId; - - const [userOrgRecord] = await db - .select() - .from(userOrgs) - .where( - and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId)) - ) - .limit(1); - - if (!userOrgRecord) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Key does not have access to this user" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error checking if key has access to this user" - ) - ); - } -} diff --git a/server/middlewares/rateLimit.ts b/server/middlewares/rateLimit.ts new file mode 100644 index 00000000..2098288f --- /dev/null +++ b/server/middlewares/rateLimit.ts @@ -0,0 +1,49 @@ +import { rateLimit } from "express-rate-limit"; +import createHttpError from "http-errors"; +import { NextFunction, Request, Response } from "express"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; + +export function rateLimitMiddleware({ + windowMin, + max, + type, + skipCondition, +}: { + windowMin: number; + max: number; + type: "IP_ONLY" | "IP_AND_PATH"; + skipCondition?: (req: Request, res: Response) => boolean; +}) { + if (type === "IP_AND_PATH") { + return rateLimit({ + windowMs: windowMin * 60 * 1000, + max, + skip: skipCondition, + keyGenerator: (req: Request) => { + return `${req.ip}-${req.path}`; + }, + handler: (req: Request, res: Response, next: NextFunction) => { + const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; + logger.warn( + `Rate limit exceeded for IP ${req.ip} on path ${req.path}`, + ); + return next( + createHttpError(HttpCode.TOO_MANY_REQUESTS, message), + ); + }, + }); + } + return rateLimit({ + windowMs: windowMin * 60 * 1000, + max, + skip: skipCondition, + handler: (req: Request, res: Response, next: NextFunction) => { + const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; + logger.warn(`Rate limit exceeded for IP ${req.ip}`); + return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); + }, + }); +} + +export default rateLimitMiddleware; diff --git a/server/middlewares/requestTimeout.ts b/server/middlewares/requestTimeout.ts deleted file mode 100644 index 8b5852b7..00000000 --- a/server/middlewares/requestTimeout.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import logger from '@server/logger'; -import createHttpError from 'http-errors'; -import HttpCode from '@server/types/HttpCode'; - -export function requestTimeoutMiddleware(timeoutMs: number = 30000) { - return (req: Request, res: Response, next: NextFunction) => { - // Set a timeout for the request - const timeout = setTimeout(() => { - if (!res.headersSent) { - logger.error(`Request timeout: ${req.method} ${req.url} from ${req.ip}`); - return next( - createHttpError( - HttpCode.REQUEST_TIMEOUT, - 'Request timeout - operation took too long to complete' - ) - ); - } - }, timeoutMs); - - // Clear timeout when response finishes - res.on('finish', () => { - clearTimeout(timeout); - }); - - // Clear timeout when response closes - res.on('close', () => { - clearTimeout(timeout); - }); - - next(); - }; -} - -export default requestTimeoutMiddleware; diff --git a/server/middlewares/stripDuplicateSessions.ts b/server/middlewares/stripDuplicateSessions.ts deleted file mode 100644 index 2558e511..00000000 --- a/server/middlewares/stripDuplicateSessions.ts +++ /dev/null @@ -1,49 +0,0 @@ -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/middlewares/verifyAccessTokenAccess.ts b/server/middlewares/verifyAccessTokenAccess.ts index 457548ae..66c84391 100644 --- a/server/middlewares/verifyAccessTokenAccess.ts +++ b/server/middlewares/verifyAccessTokenAccess.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { resourceAccessToken, resources, userOrgs } from "@server/db"; +import { resourceAccessToken, resources, userOrgs } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyAdmin.ts b/server/middlewares/verifyAdmin.ts index 22863e12..240888e2 100644 --- a/server/middlewares/verifyAdmin.ts +++ b/server/middlewares/verifyAdmin.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { roles, userOrgs } from "@server/db"; +import { roles, userOrgs } from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts deleted file mode 100644 index ba3717f0..00000000 --- a/server/middlewares/verifyApiKeyAccess.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs, apiKeys, apiKeyOrg } from "@server/db"; -import { and, eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyApiKeyAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const userId = req.user!.userId; - const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!apiKeyId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") - ); - } - - const [apiKey] = await db - .select() - .from(apiKeys) - .innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)) - .where( - and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) - ) - .limit(1); - - if (!apiKey.apiKeys) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `API key with ID ${apiKeyId} not found` - ) - ); - } - - if (!apiKeyOrg.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `API key with ID ${apiKeyId} does not have an organization ID` - ) - ); - } - - if (!req.userOrg) { - const userOrgRes = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, apiKeyOrg.orgId) - ) - ); - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); - } - - if (!req.userOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying key access" - ) - ); - } -} diff --git a/server/middlewares/verifyClientAccess.ts b/server/middlewares/verifyClientAccess.ts deleted file mode 100644 index e46d3452..00000000 --- a/server/middlewares/verifyClientAccess.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { userOrgs, clients, roleClients, userClients } from "@server/db"; -import { and, eq, inArray } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyClientAccess( - req: Request, - res: Response, - next: NextFunction -) { - const userId = req.user!.userId; // Assuming you have user information in the request - const clientId = parseInt( - req.params.clientId || req.body.clientId || req.query.clientId - ); - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - if (isNaN(clientId)) { - return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID")); - } - - try { - // Get the client - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (!client) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Client with ID ${clientId} not found` - ) - ); - } - - if (!client.orgId) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - `Client with ID ${clientId} does not have an organization ID` - ) - ); - } - - if (!req.userOrg) { - // Get user's role ID in the organization - const userOrgRes = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, client.orgId) - ) - ); - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); - } - - if (!req.userOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) - ); - } - - req.userOrgId = client.orgId; - - // Check role-based site access first - const [roleClientAccess] = await db - .select() - .from(roleClients) - .where( - and( - eq(roleClients.clientId, clientId), - inArray(roleClients.roleId, req.userRoleIds!) - ) - ) - .limit(1); - - if (roleClientAccess) { - // User has access to the site through their role - return next(); - } - - // If role doesn't have access, check user-specific site access - const [userClientAccess] = await db - .select() - .from(userClients) - .where( - and( - eq(userClients.userId, userId), - eq(userClients.clientId, clientId) - ) - ) - .limit(1); - - if (userClientAccess) { - // User has direct access to the site - return next(); - } - - // If we reach here, the user doesn't have access to the site - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this client" - ) - ); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site access" - ) - ); - } -} diff --git a/server/middlewares/verifyClientsEnabled.ts b/server/middlewares/verifyClientsEnabled.ts deleted file mode 100644 index 6e8070da..00000000 --- a/server/middlewares/verifyClientsEnabled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import config from "@server/lib/config"; - -export async function verifyClientsEnabled( - req: Request, - res: Response, - next: NextFunction -) { - try { - if (!config.getRawConfig().flags?.enable_clients) { - return next( - createHttpError( - HttpCode.NOT_IMPLEMENTED, - "Clients are not enabled on this server." - ) - ); - } - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to check if clients are enabled" - ) - ); - } -} diff --git a/server/middlewares/verifyDomainAccess.ts b/server/middlewares/verifyDomainAccess.ts deleted file mode 100644 index 8980fb9f..00000000 --- a/server/middlewares/verifyDomainAccess.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db, domains, orgDomains } from "@server/db"; -import { userOrgs, apiKeyOrg } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; - -export async function verifyDomainAccess( - req: Request, - res: Response, - next: NextFunction -) { - try { - const userId = req.user!.userId; - const domainId = - req.params.domainId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - if (!orgId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") - ); - } - - if (!domainId) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID") - ); - } - - const [domain] = await db - .select() - .from(domains) - .innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId)) - .where( - and( - eq(orgDomains.domainId, domainId), - eq(orgDomains.orgId, orgId) - ) - ) - .limit(1); - - if (!domain.orgDomains) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Domain with ID ${domainId} not found` - ) - ); - } - - if (!req.userOrg) { - const userOrgRes = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, apiKeyOrg.orgId) - ) - ); - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); - } - - if (!req.userOrg) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) - ); - } - - return next(); - } catch (error) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying domain access" - ) - ); - } -} diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index 1ea6087d..9af4fe5d 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { userOrgs } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyResourceAccess.ts b/server/middlewares/verifyResourceAccess.ts index 52216e94..43ab908e 100644 --- a/server/middlewares/verifyResourceAccess.ts +++ b/server/middlewares/verifyResourceAccess.ts @@ -4,8 +4,8 @@ import { resources, userOrgs, userResources, - roleResources, -} from "@server/db"; + roleResources +} from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -51,7 +51,7 @@ export async function verifyResourceAccess( } if (!req.userOrg) { - const userOrgRes = await db + const userOrgRole = await db .select() .from(userOrgs) .where( @@ -60,8 +60,8 @@ export async function verifyResourceAccess( eq(userOrgs.orgId, resource[0].orgId) ) ); - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); + req.userOrg = userOrgRole[0]; + req.userRoleIds = userOrgRole.map((r) => r.roleId); } if (!req.userOrg) { diff --git a/server/middlewares/verifyRoleAccess.ts b/server/middlewares/verifyRoleAccess.ts index 7ab330ec..fac348d6 100644 --- a/server/middlewares/verifyRoleAccess.ts +++ b/server/middlewares/verifyRoleAccess.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { roles, userOrgs } from "@server/db"; +import { roles, userOrgs } from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -22,7 +22,7 @@ export async function verifyRoleAccess( ); } - const roleIds = req.body?.roleIds; + const { roleIds } = req.body; const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]); if (allRoleIds.length === 0) { @@ -48,7 +48,7 @@ export async function verifyRoleAccess( // Check user access to each role's organization for (const role of rolesData) { - const userOrgRes = await db + const userOrgRole = await db .select() .from(userOrgs) .where( @@ -56,9 +56,10 @@ export async function verifyRoleAccess( eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId!) ) - ); + ) + .limit(1); - if (userOrgRes.length === 0) { + if (userOrgRole.length === 0) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -68,8 +69,6 @@ export async function verifyRoleAccess( } req.userOrgId = role.orgId; - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); } if (orgIds.size > 1) { diff --git a/server/middlewares/verifySession.ts b/server/middlewares/verifySession.ts index 6af34e4c..9d284394 100644 --- a/server/middlewares/verifySession.ts +++ b/server/middlewares/verifySession.ts @@ -1,7 +1,7 @@ import { NextFunction, Response } from "express"; import ErrorResponse from "@server/types/ErrorResponse"; import { db } from "@server/db"; -import { users } from "@server/db"; +import { users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifySetResourceUsers.ts b/server/middlewares/verifySetResourceUsers.ts index be6d21fc..0f351069 100644 --- a/server/middlewares/verifySetResourceUsers.ts +++ b/server/middlewares/verifySetResourceUsers.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { userOrgs } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; import { and, eq, inArray, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifySiteAccess.ts b/server/middlewares/verifySiteAccess.ts index 14469f77..640985de 100644 --- a/server/middlewares/verifySiteAccess.ts +++ b/server/middlewares/verifySiteAccess.ts @@ -5,8 +5,8 @@ import { userOrgs, userSites, roleSites, - roles, -} from "@server/db"; + roles +} from "@server/db/schemas"; import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -60,7 +60,7 @@ export async function verifySiteAccess( if (!req.userOrg) { // Get user's role ID in the organization - const userOrgRes = await db + const userOrgRole = await db .select() .from(userOrgs) .where( @@ -68,9 +68,10 @@ export async function verifySiteAccess( eq(userOrgs.userId, userId), eq(userOrgs.orgId, site[0].orgId) ) - ); - req.userOrg = userOrgRes[0]; - req.userRoleIds = userOrgRes.map((r) => r.roleId); + ) + .limit(1); + req.userOrg = userOrgRole[0]; + req.userRoleIds = userOrgRole.map((r) => r.roleId); } if (!req.userOrg) { diff --git a/server/middlewares/verifySiteResourceAccess.ts b/server/middlewares/verifySiteResourceAccess.ts deleted file mode 100644 index e7fefd24..00000000 --- a/server/middlewares/verifySiteResourceAccess.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { siteResources } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import logger from "@server/logger"; - -export async function verifySiteResourceAccess( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const siteResourceId = parseInt(req.params.siteResourceId); - const siteId = parseInt(req.params.siteId); - const orgId = req.params.orgId; - - if (!siteResourceId || !siteId || !orgId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Missing required parameters" - ) - ); - } - - // Check if the site resource exists and belongs to the specified site and org - const [siteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); - - if (!siteResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) - ); - } - - // Attach the siteResource to the request for use in the next middleware/route - // @ts-ignore - Extending Request type - req.siteResource = siteResource; - - next(); - } catch (error) { - logger.error("Error verifying site resource access:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Error verifying site resource access" - ) - ); - } -} diff --git a/server/middlewares/verifyTargetAccess.ts b/server/middlewares/verifyTargetAccess.ts index 424812ac..4065ce52 100644 --- a/server/middlewares/verifyTargetAccess.ts +++ b/server/middlewares/verifyTargetAccess.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { resources, targets, userOrgs } from "@server/db"; +import { resources, targets, userOrgs } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyUser.ts b/server/middlewares/verifyUser.ts index 8fd38b24..06b08601 100644 --- a/server/middlewares/verifyUser.ts +++ b/server/middlewares/verifyUser.ts @@ -1,7 +1,7 @@ import { NextFunction, Response } from "express"; import ErrorResponse from "@server/types/ErrorResponse"; import { db } from "@server/db"; -import { users } from "@server/db"; +import { users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyUserAccess.ts b/server/middlewares/verifyUserAccess.ts index 7375ad76..9cc30cf1 100644 --- a/server/middlewares/verifyUserAccess.ts +++ b/server/middlewares/verifyUserAccess.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { userOrgs } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; import { and, eq, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/middlewares/verifyUserIsOrgOwner.ts b/server/middlewares/verifyUserIsOrgOwner.ts index 318c82ec..c1d766e4 100644 --- a/server/middlewares/verifyUserIsOrgOwner.ts +++ b/server/middlewares/verifyUserIsOrgOwner.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { userOrgs } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/nextServer.ts b/server/nextServer.ts index 78169f03..e12c06e6 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -3,7 +3,6 @@ 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; @@ -16,9 +15,7 @@ export async function createNextServer() { const nextServer = express(); - nextServer.use(stripDuplicateSesions); - - nextServer.all("/{*splat}", (req, res) => { + nextServer.all("*", (req, res) => { const parsedUrl = parse(req.url!, true); return handle(req, res, parsedUrl); }); diff --git a/server/openApi.ts b/server/openApi.ts index 32cdb67b..4df6cbdd 100644 --- a/server/openApi.ts +++ b/server/openApi.ts @@ -14,6 +14,5 @@ export enum OpenAPITags { AccessToken = "Access Token", Idp = "Identity Provider", Client = "Client", - ApiKey = "API Key", - Domain = "Domain" + ApiKey = "API Key" } diff --git a/server/routers/accessToken/deleteAccessToken.ts b/server/routers/accessToken/deleteAccessToken.ts index 60d8789e..783c5fc8 100644 --- a/server/routers/accessToken/deleteAccessToken.ts +++ b/server/routers/accessToken/deleteAccessToken.ts @@ -5,9 +5,9 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { resourceAccessToken } from "@server/db"; +import { resourceAccessToken } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; -import { db } from "@server/db"; +import db from "@server/db"; import { OpenAPITags, registry } from "@server/openApi"; const deleteAccessTokenParamsSchema = z diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index 631b5924..738c230e 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -4,12 +4,12 @@ import { generateIdFromEntropySize, SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app"; -import { db } from "@server/db"; +import db from "@server/db"; import { ResourceAccessToken, resourceAccessToken, resources -} from "@server/db"; +} from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index b15041e4..daa09a4d 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -7,7 +7,7 @@ import { roleResources, resourceAccessToken, sites -} from "@server/db"; +} from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -222,7 +222,7 @@ export async function listAccessTokens( (resource) => resource.resourceId ); - const countQuery: any = db + let countQuery: any = db .select({ count: count() }) .from(resources) .where(inArray(resources.resourceId, accessibleResourceIds)); diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts deleted file mode 100644 index d61a364b..00000000 --- a/server/routers/apiKeys/createOrgApiKey.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { db } from "@server/db"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { apiKeyOrg, apiKeys } from "@server/db"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -import response from "@server/lib/response"; -import moment from "moment"; -import { - generateId, - generateIdFromEntropySize -} from "@server/auth/sessions/app"; -import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - orgId: z.string().nonempty() -}); - -const bodySchema = z.object({ - name: z.string().min(1).max(255) -}); - -export type CreateOrgApiKeyBody = z.infer; - -export type CreateOrgApiKeyResponse = { - apiKeyId: string; - name: string; - apiKey: string; - lastChars: string; - createdAt: string; -}; - -registry.registerPath({ - method: "put", - path: "/org/{orgId}/api-key", - description: "Create a new API key scoped to the organization.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - -export async function createOrgApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedParams = paramsSchema.safeParse(req.params); - - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = bodySchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - const { name } = parsedBody.data; - - const apiKeyId = generateId(15); - const apiKey = generateIdFromEntropySize(25); - const apiKeyHash = await hashPassword(apiKey); - const lastChars = apiKey.slice(-4); - const createdAt = moment().toISOString(); - - await db.transaction(async (trx) => { - await trx.insert(apiKeys).values({ - name, - apiKeyId, - apiKeyHash, - createdAt, - lastChars - }); - - await trx.insert(apiKeyOrg).values({ - apiKeyId, - orgId - }); - }); - - try { - return response(res, { - data: { - apiKeyId, - apiKey, - name, - lastChars, - createdAt - }, - success: true, - error: false, - message: "API key created", - status: HttpCode.CREATED - }); - } catch (e) { - logger.error(e); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create API key" - ) - ); - } -} diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts deleted file mode 100644 index 0754574a..00000000 --- a/server/routers/apiKeys/createRootApiKey.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { db } from "@server/db"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { apiKeyOrg, apiKeys, orgs } from "@server/db"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -import response from "@server/lib/response"; -import moment from "moment"; -import { - generateId, - generateIdFromEntropySize -} from "@server/auth/sessions/app"; -import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; - -const bodySchema = z - .object({ - name: z.string().min(1).max(255) - }) - .strict(); - -export type CreateRootApiKeyBody = z.infer; - -export type CreateRootApiKeyResponse = { - apiKeyId: string; - name: string; - apiKey: string; - lastChars: string; - createdAt: string; -}; - -export async function createRootApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = bodySchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { name } = parsedBody.data; - - const apiKeyId = generateId(15); - const apiKey = generateIdFromEntropySize(25); - const apiKeyHash = await hashPassword(apiKey); - const lastChars = apiKey.slice(-4); - const createdAt = moment().toISOString(); - - await db.transaction(async (trx) => { - await trx.insert(apiKeys).values({ - apiKeyId, - name, - apiKeyHash, - createdAt, - lastChars, - isRoot: true - }); - }); - - try { - return response(res, { - data: { - apiKeyId, - name, - apiKey, - lastChars, - createdAt - }, - success: true, - error: false, - message: "API key created", - status: HttpCode.CREATED - }); - } catch (e) { - logger.error(e); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create API key" - ) - ); - } -} diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts deleted file mode 100644 index 4b97b353..00000000 --- a/server/routers/apiKeys/deleteApiKey.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeys } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -registry.registerPath({ - method: "delete", - path: "/org/{orgId}/api-key/{apiKeyId}", - description: "Delete an API key.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema - }, - responses: {} -}); - -export async function deleteApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { apiKeyId } = parsedParams.data; - - const [apiKey] = await db - .select() - .from(apiKeys) - .where(eq(apiKeys.apiKeyId, apiKeyId)) - .limit(1); - - if (!apiKey) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `API Key with ID ${apiKeyId} not found` - ) - ); - } - - await db.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId)); - - return response(res, { - data: null, - success: true, - error: false, - message: "API key deleted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/deleteOrgApiKey.ts b/server/routers/apiKeys/deleteOrgApiKey.ts deleted file mode 100644 index 22e776ca..00000000 --- a/server/routers/apiKeys/deleteOrgApiKey.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeyOrg, apiKeys } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty(), - orgId: z.string().nonempty() -}); - -export async function deleteOrgApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { apiKeyId, orgId } = parsedParams.data; - - const [apiKey] = await db - .select() - .from(apiKeys) - .where(eq(apiKeys.apiKeyId, apiKeyId)) - .innerJoin( - apiKeyOrg, - and( - eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ) - .limit(1); - - if (!apiKey) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `API Key with ID ${apiKeyId} not found` - ) - ); - } - - if (apiKey.apiKeys.isRoot) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Cannot delete root API key" - ) - ); - } - - await db.transaction(async (trx) => { - await trx - .delete(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKeyId), - eq(apiKeyOrg.orgId, orgId) - ) - ); - - const apiKeyOrgs = await db - .select() - .from(apiKeyOrg) - .where(eq(apiKeyOrg.apiKeyId, apiKeyId)); - - if (apiKeyOrgs.length === 0) { - await trx.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId)); - } - }); - - return response(res, { - data: null, - success: true, - error: false, - message: "API removed from organization", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/getApiKey.ts b/server/routers/apiKeys/getApiKey.ts deleted file mode 100644 index 2bb3b65c..00000000 --- a/server/routers/apiKeys/getApiKey.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeys } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -async function query(apiKeyId: string) { - return await db - .select({ - apiKeyId: apiKeys.apiKeyId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - isRoot: apiKeys.isRoot, - name: apiKeys.name - }) - .from(apiKeys) - .where(eq(apiKeys.apiKeyId, apiKeyId)) - .limit(1); -} - -export type GetApiKeyResponse = NonNullable< - Awaited>[0] ->; - -export async function getApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { apiKeyId } = parsedParams.data; - - const [apiKey] = await query(apiKeyId); - - if (!apiKey) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `API Key with ID ${apiKeyId} not found` - ) - ); - } - - return response(res, { - data: apiKey, - success: true, - error: false, - message: "API key deleted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/index.ts b/server/routers/apiKeys/index.ts deleted file mode 100644 index 62ede75c..00000000 --- a/server/routers/apiKeys/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./createRootApiKey"; -export * from "./deleteApiKey"; -export * from "./getApiKey"; -export * from "./listApiKeyActions"; -export * from "./listOrgApiKeys"; -export * from "./listApiKeyActions"; -export * from "./listRootApiKeys"; -export * from "./setApiKeyActions"; -export * from "./setApiKeyOrgs"; -export * from "./createOrgApiKey"; -export * from "./deleteOrgApiKey"; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts deleted file mode 100644 index 51d20b24..00000000 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { db } from "@server/db"; -import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -function queryActions(apiKeyId: string) { - return db - .select({ - actionId: actions.actionId - }) - .from(apiKeyActions) - .where(eq(apiKeyActions.apiKeyId, apiKeyId)) - .innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId)); -} - -export type ListApiKeyActionsResponse = { - actions: Awaited>; - pagination: { total: number; limit: number; offset: number }; -}; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/api-key/{apiKeyId}/actions", - description: - "List all actions set for an API key.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema, - query: querySchema - }, - responses: {} -}); - -export async function listApiKeyActions( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedQuery = querySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error) - ) - ); - } - - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error) - ) - ); - } - - const { limit, offset } = parsedQuery.data; - const { apiKeyId } = parsedParams.data; - - const baseQuery = queryActions(apiKeyId); - - const actionsList = await baseQuery.limit(limit).offset(offset); - - return response(res, { - data: { - actions: actionsList, - pagination: { - total: actionsList.length, - limit, - offset - } - }, - success: true, - error: false, - message: "API keys retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts deleted file mode 100644 index e8c8bc1c..00000000 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { db } from "@server/db"; -import { apiKeyOrg, apiKeys } from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq, and } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -const paramsSchema = z.object({ - orgId: z.string() -}); - -function queryApiKeys(orgId: string) { - return db - .select({ - apiKeyId: apiKeys.apiKeyId, - orgId: apiKeyOrg.orgId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - name: apiKeys.name - }) - .from(apiKeyOrg) - .where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false))) - .innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)); -} - -export type ListOrgApiKeysResponse = { - apiKeys: Awaited>; - pagination: { total: number; limit: number; offset: number }; -}; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/api-keys", - description: "List all API keys for an organization", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema, - query: querySchema - }, - responses: {} -}); - -export async function listOrgApiKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedQuery = querySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error) - ) - ); - } - - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error) - ) - ); - } - - const { limit, offset } = parsedQuery.data; - const { orgId } = parsedParams.data; - - const baseQuery = queryApiKeys(orgId); - - const apiKeysList = await baseQuery.limit(limit).offset(offset); - - return response(res, { - data: { - apiKeys: apiKeysList, - pagination: { - total: apiKeysList.length, - limit, - offset - } - }, - success: true, - error: false, - message: "API keys retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts deleted file mode 100644 index ddfade3c..00000000 --- a/server/routers/apiKeys/listRootApiKeys.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { db } from "@server/db"; -import { apiKeys } from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { eq } from "drizzle-orm"; - -const querySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -function queryApiKeys() { - return db - .select({ - apiKeyId: apiKeys.apiKeyId, - lastChars: apiKeys.lastChars, - createdAt: apiKeys.createdAt, - name: apiKeys.name - }) - .from(apiKeys) - .where(eq(apiKeys.isRoot, true)); -} - -export type ListRootApiKeysResponse = { - apiKeys: Awaited>; - pagination: { total: number; limit: number; offset: number }; -}; - -export async function listRootApiKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedQuery = querySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error) - ) - ); - } - const { limit, offset } = parsedQuery.data; - - const baseQuery = queryApiKeys(); - - const apiKeysList = await baseQuery.limit(limit).offset(offset); - - return response(res, { - data: { - apiKeys: apiKeysList, - pagination: { - total: apiKeysList.length, - limit, - offset - } - }, - success: true, - error: false, - message: "API keys retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts deleted file mode 100644 index bb16deb5..00000000 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { actions, apiKeyActions } from "@server/db"; -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 { eq, and, inArray } from "drizzle-orm"; -import { OpenAPITags, registry } from "@server/openApi"; - -const bodySchema = z - .object({ - actionIds: z - .array(z.string().nonempty()) - .transform((v) => Array.from(new Set(v))) - }) - .strict(); - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/api-key/{apiKeyId}/actions", - description: - "Set actions for an API key. This will replace any existing actions.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - -export async function setApiKeyActions( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { actionIds: newActionIds } = parsedBody.data; - - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { apiKeyId } = parsedParams.data; - - const actionsExist = await db - .select() - .from(actions) - .where(inArray(actions.actionId, newActionIds)); - - if (actionsExist.length !== newActionIds.length) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "One or more actions do not exist" - ) - ); - } - - await db.transaction(async (trx) => { - const existingActions = await trx - .select() - .from(apiKeyActions) - .where(eq(apiKeyActions.apiKeyId, apiKeyId)); - - const existingActionIds = existingActions.map((a) => a.actionId); - - const actionIdsToAdd = newActionIds.filter( - (id) => !existingActionIds.includes(id) - ); - const actionIdsToRemove = existingActionIds.filter( - (id) => !newActionIds.includes(id) - ); - - if (actionIdsToRemove.length > 0) { - await trx - .delete(apiKeyActions) - .where( - and( - eq(apiKeyActions.apiKeyId, apiKeyId), - inArray(apiKeyActions.actionId, actionIdsToRemove) - ) - ); - } - - if (actionIdsToAdd.length > 0) { - const insertValues = actionIdsToAdd.map((actionId) => ({ - apiKeyId, - actionId - })); - await trx.insert(apiKeyActions).values(insertValues); - } - }); - - return response(res, { - data: {}, - success: true, - error: false, - message: "API key actions updated successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts deleted file mode 100644 index f03eec18..00000000 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { apiKeyOrg, orgs } from "@server/db"; -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 { eq, and, inArray } from "drizzle-orm"; - -const bodySchema = z - .object({ - orgIds: z - .array(z.string().nonempty()) - .transform((v) => Array.from(new Set(v))) - }) - .strict(); - -const paramsSchema = z.object({ - apiKeyId: z.string().nonempty() -}); - -export async function setApiKeyOrgs( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { orgIds: newOrgIds } = parsedBody.data; - - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { apiKeyId } = parsedParams.data; - - // make sure all orgs exist - const allOrgs = await db - .select() - .from(orgs) - .where(inArray(orgs.orgId, newOrgIds)); - - if (allOrgs.length !== newOrgIds.length) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "One or more orgs do not exist" - ) - ); - } - - await db.transaction(async (trx) => { - const existingOrgs = await trx - .select({ orgId: apiKeyOrg.orgId }) - .from(apiKeyOrg) - .where(eq(apiKeyOrg.apiKeyId, apiKeyId)); - - const existingOrgIds = existingOrgs.map((a) => a.orgId); - - const orgIdsToAdd = newOrgIds.filter( - (id) => !existingOrgIds.includes(id) - ); - const orgIdsToRemove = existingOrgIds.filter( - (id) => !newOrgIds.includes(id) - ); - - if (orgIdsToRemove.length > 0) { - await trx - .delete(apiKeyOrg) - .where( - and( - eq(apiKeyOrg.apiKeyId, apiKeyId), - inArray(apiKeyOrg.orgId, orgIdsToRemove) - ) - ); - } - - if (orgIdsToAdd.length > 0) { - const insertValues = orgIdsToAdd.map((orgId) => ({ - apiKeyId, - orgId - })); - await trx.insert(apiKeyOrg).values(insertValues); - } - - return response(res, { - data: {}, - success: true, - error: false, - message: "API key orgs updated successfully", - status: HttpCode.OK - }); - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts index 64efb696..3b1e4c2f 100644 --- a/server/routers/auth/changePassword.ts +++ b/server/routers/auth/changePassword.ts @@ -4,9 +4,9 @@ import HttpCode from "@server/types/HttpCode"; import { fromError } from "zod-validation-error"; import { z } from "zod"; import { db } from "@server/db"; -import { User, users } from "@server/db"; +import { User, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; -import { response } from "@server/lib/response"; +import { response } from "@server/lib"; import { hashPassword, verifyPassword diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts index 9840d564..ca7d80cc 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/response"; +import { response } from "@server/lib"; 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 da19c0d7..b10dd9b2 100644 --- a/server/routers/auth/disable2fa.ts +++ b/server/routers/auth/disable2fa.ts @@ -4,9 +4,9 @@ import HttpCode from "@server/types/HttpCode"; import { fromError } from "zod-validation-error"; import { z } from "zod"; import { db } from "@server/db"; -import { User, users } from "@server/db"; +import { User, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; -import { response } from "@server/lib/response"; +import { response } from "@server/lib"; 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 754478fc..b2eaf8d2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -6,11 +6,7 @@ export * from "./requestTotpSecret"; export * from "./disable2fa"; export * from "./verifyEmail"; export * from "./requestEmailVerificationCode"; -export * from "./resetPassword"; -export * from "./requestPasswordReset"; -export * from "./setServerAdmin"; -export * from "./initialSetupComplete"; -export * from "./validateSetupToken"; export * from "./changePassword"; +export * from "./requestPasswordReset"; +export * from "./resetPassword"; export * from "./checkResourceSession"; -export * from "./securityKey"; \ No newline at end of file diff --git a/server/routers/auth/initialSetupComplete.ts b/server/routers/auth/initialSetupComplete.ts deleted file mode 100644 index 2b616c97..00000000 --- a/server/routers/auth/initialSetupComplete.ts +++ /dev/null @@ -1,42 +0,0 @@ -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/response"; -import { db, users } from "@server/db"; -import { eq } from "drizzle-orm"; - -export type InitialSetupCompleteResponse = { - complete: boolean; -}; - -export async function initialSetupComplete( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const [existing] = await db - .select() - .from(users) - .where(eq(users.serverAdmin, true)); - - return response(res, { - data: { - complete: !!existing - }, - success: true, - error: false, - message: "Initial setup check completed", - status: HttpCode.OK - }); - } catch (e) { - logger.error(e); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to check initial setup completion" - ) - ); - } -} diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 8dad5a42..eda637fa 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -3,8 +3,8 @@ import { generateSessionToken, serializeSessionCookie } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { users, securityKeys } from "@server/db"; +import db from "@server/db"; +import { users } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq, and } from "drizzle-orm"; @@ -21,7 +21,10 @@ import { UserType } from "@server/types/UserTypes"; export const loginBodySchema = z .object({ - email: z.string().toLowerCase().email(), + email: z + .string() + .email() + .transform((v) => v.toLowerCase()), password: z.string(), code: z.string().optional() }) @@ -32,8 +35,6 @@ export type LoginBody = z.infer; export type LoginResponse = { codeRequested?: boolean; emailVerificationRequired?: boolean; - useSecurityKey?: boolean; - twoFactorSetupRequired?: boolean; }; export async function login( @@ -106,35 +107,6 @@ export async function login( ); } - // // Check if user has security keys registered - // const userSecurityKeys = await db - // .select() - // .from(securityKeys) - // .where(eq(securityKeys.userId, existingUser.userId)); - // - // if (userSecurityKeys.length > 0) { - // return response(res, { - // data: { useSecurityKey: true }, - // success: true, - // error: false, - // message: "Security key authentication required", - // status: HttpCode.OK - // }); - // } - - if ( - existingUser.twoFactorSetupRequested && - !existingUser.twoFactorEnabled - ) { - return response(res, { - data: { twoFactorSetupRequired: true }, - success: true, - error: false, - message: "Two-factor authentication setup required", - status: HttpCode.ACCEPTED - }); - } - if (existingUser.twoFactorEnabled) { if (!code) { return response<{ codeRequested: boolean }>(res, { diff --git a/server/routers/auth/logout.ts b/server/routers/auth/logout.ts index b9a1431a..db95c2e6 100644 --- a/server/routers/auth/logout.ts +++ b/server/routers/auth/logout.ts @@ -34,7 +34,7 @@ export async function logout( try { await invalidateSession(session.sessionId); } catch (error) { - logger.error("Failed to invalidate session", error); + logger.error("Failed to invalidate session", error) } const isSecure = req.protocol === "https"; diff --git a/server/routers/auth/requestEmailVerificationCode.ts b/server/routers/auth/requestEmailVerificationCode.ts index 7358e6ed..0cc8825c 100644 --- a/server/routers/auth/requestEmailVerificationCode.ts +++ b/server/routers/auth/requestEmailVerificationCode.ts @@ -1,8 +1,8 @@ import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib/response"; -import { User } from "@server/db"; +import { response } from "@server/lib"; +import { User } from "@server/db/schemas"; import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode"; import config from "@server/lib/config"; import logger from "@server/logger"; diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index 52dce2e3..087352f0 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -3,9 +3,9 @@ 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/response"; +import { response } from "@server/lib"; import { db } from "@server/db"; -import { passwordResetTokens, users } from "@server/db"; +import { passwordResetTokens, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { alphabet, generateRandomString, sha256 } from "oslo/crypto"; import { createDate } from "oslo"; @@ -20,8 +20,8 @@ export const requestPasswordResetBody = z .object({ email: z .string() - .toLowerCase() - .email(), + .email() + .transform((v) => v.toLowerCase()) }) .strict(); diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts index 7c122a44..a4f8bc4a 100644 --- a/server/routers/auth/requestTotpSecret.ts +++ b/server/routers/auth/requestTotpSecret.ts @@ -4,22 +4,19 @@ 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/response"; +import { response } from "@server/lib"; import { db } from "@server/db"; -import { User, users } from "@server/db"; -import { eq, and } from "drizzle-orm"; +import { User, users } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; import { createTOTPKeyURI } from "oslo/otp"; import logger from "@server/logger"; import { verifyPassword } from "@server/auth/password"; import { unauthorized } from "@server/auth/unauthorizedResponse"; import { UserType } from "@server/types/UserTypes"; -import { verifySession } from "@server/auth/sessions/verifySession"; -import config from "@server/lib/config"; export const requestTotpSecretBody = z .object({ - password: z.string(), - email: z.string().email().optional() + password: z.string() }) .strict(); @@ -46,42 +43,9 @@ export async function requestTotpSecret( ); } - const { password, email } = parsedBody.data; + const { password } = parsedBody.data; - const { user: sessionUser, session: existingSession } = await verifySession(req); - - let user: User | null = sessionUser; - if (!existingSession) { - if (!email) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Email is required for two-factor authentication setup" - ) - ); - } - const [res] = await db - .select() - .from(users) - .where( - and(eq(users.type, UserType.Internal), eq(users.email, email)) - ); - user = res; - } - - if (!user) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Username or password incorrect. Email: ${email}. IP: ${req.ip}.` - ); - } - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - "Username or password is incorrect" - ) - ); - } + const user = req.user as User; if (user.type !== UserType.Internal) { return next( @@ -93,10 +57,7 @@ export async function requestTotpSecret( } try { - const validPassword = await verifyPassword( - password, - user.passwordHash! - ); + const validPassword = await verifyPassword(password, user.passwordHash!); if (!validPassword) { return next(unauthorized()); } @@ -110,15 +71,9 @@ 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( - appName, - user.email!, - hex - ); + const uri = createTOTPKeyURI("Pangolin", user.email!, hex); await db .update(users) diff --git a/server/routers/auth/resetPassword.ts b/server/routers/auth/resetPassword.ts index 05293727..967ddc66 100644 --- a/server/routers/auth/resetPassword.ts +++ b/server/routers/auth/resetPassword.ts @@ -4,9 +4,9 @@ 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/response"; +import { response } from "@server/lib"; import { db } from "@server/db"; -import { passwordResetTokens, users } from "@server/db"; +import { passwordResetTokens, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { hashPassword, verifyPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/totp"; @@ -21,8 +21,8 @@ export const resetPasswordBody = z .object({ email: z .string() - .toLowerCase() - .email(), + .email() + .transform((v) => v.toLowerCase()), token: z.string(), // reset secret code newPassword: passwordSchema, code: z.string().optional() // 2fa code diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts deleted file mode 100644 index 1e75764b..00000000 --- a/server/routers/auth/securityKey.ts +++ /dev/null @@ -1,706 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import { fromError } from "zod-validation-error"; -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/response"; -import logger from "@server/logger"; -import { - generateRegistrationOptions, - verifyRegistrationResponse, - generateAuthenticationOptions, - verifyAuthenticationResponse -} from "@simplewebauthn/server"; -import type { - GenerateRegistrationOptionsOpts, - GenerateAuthenticationOptionsOpts, - AuthenticatorTransportFuture -} from "@simplewebauthn/server"; -import { - isoBase64URL -} from '@simplewebauthn/server/helpers'; -import config from "@server/lib/config"; -import { UserType } from "@server/types/UserTypes"; -import { verifyPassword } from "@server/auth/password"; -import { unauthorized } from "@server/auth/unauthorizedResponse"; -import { verifyTotpCode } from "@server/auth/totp"; - -// The RP ID is the domain name of your application -const rpID = (() => { - const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined; - // For localhost, we must use 'localhost' without port - if (url?.hostname === 'localhost' || !url) { - return 'localhost'; - } - return url.hostname; -})(); - -const rpName = "Pangolin"; -const origin = config.getRawConfig().app.dashboard_url || "localhost"; - -// Database-based challenge storage (replaces in-memory storage) -// Challenges are stored in the webauthnChallenge table with automatic expiration -// This supports clustered deployments and persists across server restarts - -// Clean up expired challenges every 5 minutes -setInterval(async () => { - try { - const now = Date.now(); - await db - .delete(webauthnChallenge) - .where(lt(webauthnChallenge.expiresAt, now)); - logger.debug("Cleaned up expired security key challenges"); - } catch (error) { - logger.error("Failed to clean up expired security key challenges", error); - } -}, 5 * 60 * 1000); - -// Helper functions for challenge management -async function storeChallenge(sessionId: string, challenge: string, securityKeyName?: string, userId?: string) { - const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes - - // Delete any existing challenge for this session - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); - - // Insert new challenge - await db.insert(webauthnChallenge).values({ - sessionId, - challenge, - securityKeyName, - userId, - expiresAt - }); -} - -async function getChallenge(sessionId: string) { - const [challengeData] = await db - .select() - .from(webauthnChallenge) - .where(eq(webauthnChallenge.sessionId, sessionId)) - .limit(1); - - if (!challengeData) { - return null; - } - - // Check if expired - if (challengeData.expiresAt < Date.now()) { - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); - return null; - } - - return challengeData; -} - -async function clearChallenge(sessionId: string) { - await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId)); -} - -export const registerSecurityKeyBody = z.object({ - name: z.string().min(1), - password: z.string().min(1), - code: z.string().optional() -}).strict(); - -export const verifyRegistrationBody = z.object({ - credential: z.any() -}).strict(); - -export const startAuthenticationBody = z.object({ - email: z.string().email().optional() -}).strict(); - -export const verifyAuthenticationBody = z.object({ - credential: z.any() -}).strict(); - -export const deleteSecurityKeyBody = z.object({ - password: z.string().min(1), - code: z.string().optional() -}).strict(); - -export async function startRegistration( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = registerSecurityKeyBody.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { name, password, code } = parsedBody.data; - const user = req.user as User; - - // Only allow internal users to use security keys - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Security keys are only available for internal users" - ) - ); - } - - try { - // Verify password - const validPassword = await verifyPassword(password, user.passwordHash!); - if (!validPassword) { - return next(unauthorized()); - } - - // If user has 2FA enabled, require and verify the code - if (user.twoFactorEnabled) { - if (!code) { - return response<{ codeRequested: boolean }>(res, { - data: { codeRequested: true }, - success: true, - error: false, - message: "Two-factor authentication required", - status: HttpCode.ACCEPTED - }); - } - - const validOTP = await verifyTotpCode( - code, - user.twoFactorSecret!, - user.userId - ); - - if (!validOTP) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Two-factor code incorrect. Email: ${user.email}. IP: ${req.ip}.` - ); - } - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - "The two-factor code you entered is incorrect" - ) - ); - } - } - - // Get existing security keys for user - const existingSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, user.userId)); - - const excludeCredentials = existingSecurityKeys.map(key => ({ - id: key.credentialId, - transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined - })); - - const options: GenerateRegistrationOptionsOpts = { - rpName, - rpID, - userID: isoBase64URL.toBuffer(user.userId), - userName: user.email || user.username, - attestationType: 'none', - excludeCredentials, - authenticatorSelection: { - residentKey: 'preferred', - userVerification: 'preferred', - } - }; - - const registrationOptions = await generateRegistrationOptions(options); - - // Store challenge in database - await storeChallenge(req.session.sessionId, registrationOptions.challenge, name, user.userId); - - return response(res, { - data: registrationOptions, - success: true, - error: false, - message: "Registration options generated successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to start registration" - ) - ); - } -} - -export async function verifyRegistration( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = verifyRegistrationBody.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { credential } = parsedBody.data; - const user = req.user as User; - - // Only allow internal users to use security keys - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Security keys are only available for internal users" - ) - ); - } - - try { - // Get challenge from database - const challengeData = await getChallenge(req.session.sessionId); - - if (!challengeData) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "No challenge found in session or challenge expired" - ) - ); - } - - const verification = await verifyRegistrationResponse({ - response: credential, - expectedChallenge: challengeData.challenge, - expectedOrigin: origin, - expectedRPID: rpID, - requireUserVerification: false - }); - - const { verified, registrationInfo } = verification; - - if (!verified || !registrationInfo) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Verification failed" - ) - ); - } - - // Store the security key in the database - await db.insert(securityKeys).values({ - credentialId: registrationInfo.credential.id, - userId: user.userId, - 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() - }); - - // Clear challenge data - await clearChallenge(req.session.sessionId); - - return response(res, { - data: null, - success: true, - error: false, - message: "Security key registered successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to verify registration" - ) - ); - } -} - -export async function listSecurityKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - const user = req.user as User; - - // Only allow internal users to use security keys - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Security keys are only available for internal users" - ) - ); - } - - try { - const userSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, user.userId)); - - return response(res, { - data: userSecurityKeys, - success: true, - error: false, - message: "Security keys retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to retrieve security keys" - ) - ); - } -} - -export async function deleteSecurityKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - const { credentialId: encodedCredentialId } = req.params; - const credentialId = decodeURIComponent(encodedCredentialId); - const user = req.user as User; - - const parsedBody = deleteSecurityKeyBody.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { password, code } = parsedBody.data; - - // Only allow internal users to use security keys - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Security keys are only available for internal users" - ) - ); - } - - try { - // Verify password - const validPassword = await verifyPassword(password, user.passwordHash!); - if (!validPassword) { - return next(unauthorized()); - } - - // If user has 2FA enabled, require and verify the code - if (user.twoFactorEnabled) { - if (!code) { - return response<{ codeRequested: boolean }>(res, { - data: { codeRequested: true }, - success: true, - error: false, - message: "Two-factor authentication required", - status: HttpCode.ACCEPTED - }); - } - - const validOTP = await verifyTotpCode( - code, - user.twoFactorSecret!, - user.userId - ); - - if (!validOTP) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Two-factor code incorrect. Email: ${user.email}. IP: ${req.ip}.` - ); - } - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - "The two-factor code you entered is incorrect" - ) - ); - } - } - - await db - .delete(securityKeys) - .where(and( - eq(securityKeys.credentialId, credentialId), - eq(securityKeys.userId, user.userId) - )); - - return response(res, { - data: null, - success: true, - error: false, - message: "Security key deleted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to delete security key" - ) - ); - } -} - -export async function startAuthentication( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = startAuthenticationBody.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { email } = parsedBody.data; - - try { - let allowCredentials; - let userId; - - // If email is provided, get security keys for that specific user - if (email) { - const [user] = await db - .select() - .from(users) - .where(eq(users.email, email)) - .limit(1); - - if (!user || user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid credentials" - ) - ); - } - - userId = user.userId; - - const userSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, user.userId)); - - if (userSecurityKeys.length === 0) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "No security keys registered for this user" - ) - ); - } - - allowCredentials = userSecurityKeys.map(key => ({ - id: key.credentialId, - transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined - })); - } - - const options: GenerateAuthenticationOptionsOpts = { - rpID, - allowCredentials, - userVerification: 'preferred', - }; - - const authenticationOptions = await generateAuthenticationOptions(options); - - // Generate a temporary session ID for unauthenticated users - const tempSessionId = email ? `temp_${email}_${Date.now()}` : `temp_${Date.now()}`; - - // Store challenge in database - await storeChallenge(tempSessionId, authenticationOptions.challenge, undefined, userId); - - return response(res, { - data: { ...authenticationOptions, tempSessionId }, - success: true, - error: false, - message: "Authentication options generated", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to generate authentication options" - ) - ); - } -} - -export async function verifyAuthentication( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = verifyAuthenticationBody.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { credential } = parsedBody.data; - const tempSessionId = req.headers['x-temp-session-id'] as string; - - if (!tempSessionId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Your session information is missing. This might happen if you've been inactive for too long or if your browser cleared temporary data. Please start the sign-in process again." - ) - ); - } - - try { - // Get challenge from database - const challengeData = await getChallenge(tempSessionId); - - if (!challengeData) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Your sign-in session has expired. For security reasons, you have 5 minutes to complete the authentication process. Please try signing in again." - ) - ); - } - - // Find the security key in database - const credentialId = credential.id; - const [securityKey] = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.credentialId, credentialId)) - .limit(1); - - if (!securityKey) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "We couldn't verify your security key. This might happen if your device isn't compatible or if the security key was removed too quickly. Please try again and keep your security key connected until the process completes." - ) - ); - } - - // Get the user - const [user] = await db - .select() - .from(users) - .where(eq(users.userId, securityKey.userId)) - .limit(1); - - if (!user || user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "User not found or not authorized for security key authentication" - ) - ); - } - - const verification = await verifyAuthenticationResponse({ - response: credential, - expectedChallenge: challengeData.challenge, - expectedOrigin: origin, - expectedRPID: rpID, - credential: { - id: securityKey.credentialId, - publicKey: isoBase64URL.toBuffer(securityKey.publicKey), - counter: securityKey.signCount, - transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransportFuture[] : undefined - }, - requireUserVerification: false - }); - - const { verified, authenticationInfo } = verification; - - if (!verified) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Authentication failed. This could happen if your security key wasn't recognized or was removed too early. Please ensure your security key is properly connected and try again." - ) - ); - } - - // Update sign count - await db - .update(securityKeys) - .set({ - signCount: authenticationInfo.newCounter, - lastUsed: new Date().toISOString() - }) - .where(eq(securityKeys.credentialId, credentialId)); - - // Create session for the user - const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app"); - const token = generateSessionToken(); - const session = await createSession(token, user.userId); - const isSecure = req.protocol === "https"; - const cookie = serializeSessionCookie( - token, - isSecure, - new Date(session.expiresAt) - ); - - res.setHeader("Set-Cookie", cookie); - - // Clear challenge data - await clearChallenge(tempSessionId); - - return response(res, { - data: null, - success: true, - error: false, - message: "Authentication successful", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to verify authentication" - ) - ); - } -} \ No newline at end of file diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts deleted file mode 100644 index 716feca4..00000000 --- a/server/routers/auth/setServerAdmin.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import createHttpError from "http-errors"; -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/response"; -import { db, users, setupTokens } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { UserType } from "@server/types/UserTypes"; -import moment from "moment"; - -export const bodySchema = z.object({ - email: z.string().toLowerCase().email(), - password: passwordSchema, - setupToken: z.string().min(1, "Setup token is required") -}); - -export type SetServerAdminBody = z.infer; - -export type SetServerAdminResponse = null; - -export async function setServerAdmin( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = bodySchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { email, password, setupToken } = parsedBody.data; - - // Validate setup token - const [validToken] = await db - .select() - .from(setupTokens) - .where( - and( - eq(setupTokens.token, setupToken), - eq(setupTokens.used, false) - ) - ); - - if (!validToken) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid or expired setup token" - ) - ); - } - - const [existing] = await db - .select() - .from(users) - .where(eq(users.serverAdmin, true)); - - if (existing) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Server admin already exists" - ) - ); - } - - const passwordHash = await hashPassword(password); - const userId = generateId(15); - - await db.transaction(async (trx) => { - // Mark the token as used - await trx - .update(setupTokens) - .set({ - used: true, - dateUsed: moment().toISOString() - }) - .where(eq(setupTokens.tokenId, validToken.tokenId)); - - // Create the server admin user - await trx.insert(users).values({ - userId: userId, - email: email, - type: UserType.Internal, - username: email, - passwordHash, - dateCreated: moment().toISOString(), - serverAdmin: true, - emailVerified: true - }); - }); - - return response(res, { - data: null, - success: true, - error: false, - message: "Server admin set successfully", - status: HttpCode.OK - }); - } catch (e) { - logger.error(e); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to set server admin" - ) - ); - } -} diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index fe978d0d..564a1378 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -1,7 +1,8 @@ import { NextFunction, Request, Response } from "express"; -import { db, users } from "@server/db"; +import db from "@server/db"; import HttpCode from "@server/types/HttpCode"; import { z } from "zod"; +import { users } from "@server/db/schemas"; import { fromError } from "zod-validation-error"; import createHttpError from "http-errors"; import response from "@server/lib/response"; @@ -23,7 +24,10 @@ import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; export const signupBodySchema = z.object({ - email: z.string().toLowerCase().email(), + email: z + .string() + .email() + .transform((v) => v.toLowerCase()), password: passwordSchema, inviteToken: z.string().optional(), inviteId: z.string().optional() @@ -51,8 +55,9 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId } = - parsedBody.data; + const { email, password, inviteToken, inviteId } = parsedBody.data; + + logger.debug("signup", { email, password, inviteToken, inviteId }); const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -138,21 +143,15 @@ export async function signup( if (diff < 2) { // If the user was created less than 2 hours ago, we don't want to create a new user - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "A user with that email address already exists" - ) - ); - // return response(res, { - // data: { - // emailVerificationRequired: true - // }, - // success: true, - // error: false, - // message: `A user with that email address already exists. We sent an email to ${email} with a verification code.`, - // status: HttpCode.OK - // }); + return response(res, { + data: { + emailVerificationRequired: true + }, + success: true, + error: false, + message: `A user with that email address already exists. We sent an email to ${email} with a verification code.`, + status: HttpCode.OK + }); } else { // If the user was created more than 2 hours ago, we want to delete the old user and create a new one await db.delete(users).where(eq(users.userId, user.userId)); @@ -165,9 +164,7 @@ export async function signup( username: email, email: email, passwordHash, - dateCreated: moment().toISOString(), - termsAcceptedTimestamp: null, - termsVersion: "1" + dateCreated: moment().toISOString() }); // give the user their default permissions: diff --git a/server/routers/auth/types.ts b/server/routers/auth/types.ts deleted file mode 100644 index bb5a1b4e..00000000 --- a/server/routers/auth/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TransferSessionResponse = { - valid: boolean; - cookie?: string; -}; - -export type GetSessionTransferTokenRenponse = { - token: string; -}; \ No newline at end of file diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts deleted file mode 100644 index e3c29833..00000000 --- a/server/routers/auth/validateSetupToken.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, setupTokens } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -const validateSetupTokenSchema = z - .object({ - token: z.string().min(1, "Token is required") - }) - .strict(); - -export type ValidateSetupTokenResponse = { - valid: boolean; - message: string; -}; - -export async function validateSetupToken( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = validateSetupTokenSchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { token } = parsedBody.data; - - // Find the token in the database - const [setupToken] = await db - .select() - .from(setupTokens) - .where( - and( - eq(setupTokens.token, token), - eq(setupTokens.used, false) - ) - ); - - if (!setupToken) { - return response(res, { - data: { - valid: false, - message: "Invalid or expired setup token" - }, - success: true, - error: false, - message: "Token validation completed", - status: HttpCode.OK - }); - } - - return response(res, { - data: { - valid: true, - message: "Setup token is valid" - }, - success: true, - error: false, - message: "Token validation completed", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to validate setup token" - ) - ); - } -} \ No newline at end of file diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts index c624e747..fd7aa138 100644 --- a/server/routers/auth/verifyEmail.ts +++ b/server/routers/auth/verifyEmail.ts @@ -3,9 +3,9 @@ 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/response"; +import { response } from "@server/lib"; import { db } from "@server/db"; -import { User, emailVerificationCodes, users } from "@server/db"; +import { User, emailVerificationCodes, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { isWithinExpirationDate } from "oslo"; import config from "@server/lib/config"; diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts index c44c0c53..db4ec1a1 100644 --- a/server/routers/auth/verifyTotp.ts +++ b/server/routers/auth/verifyTotp.ts @@ -3,25 +3,21 @@ 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/response"; +import { response } from "@server/lib"; import { db } from "@server/db"; -import { twoFactorBackupCodes, User, users } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { hashPassword, verifyPassword } from "@server/auth/password"; +import { twoFactorBackupCodes, User, users } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import { alphabet, generateRandomString } from "oslo/crypto"; +import { hashPassword } from "@server/auth/password"; import { verifyTotpCode } from "@server/auth/totp"; import logger from "@server/logger"; import { sendEmail } from "@server/emails"; import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNotification"; import config from "@server/lib/config"; import { UserType } from "@server/types/UserTypes"; -import { generateBackupCodes } from "@server/lib/totp"; -import { verifySession } from "@server/auth/sessions/verifySession"; -import { unauthorized } from "@server/auth/unauthorizedResponse"; export const verifyTotpBody = z .object({ - email: z.string().email().optional(), - password: z.string().optional(), code: z.string() }) .strict(); @@ -49,83 +45,38 @@ export async function verifyTotp( ); } - const { code, email, password } = parsedBody.data; + const { code } = parsedBody.data; + + const user = req.user as User; + + if (user.type !== UserType.Internal) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Two-factor authentication is not supported for external users" + ) + ); + } + + if (user.twoFactorEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Two-factor authentication is already enabled" + ) + ); + } + + if (!user.twoFactorSecret) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User has not requested two-factor authentication" + ) + ); + } try { - const { user: sessionUser, session: existingSession } = - await verifySession(req); - - let user: User | null = sessionUser; - if (!existingSession) { - if (!email || !password) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Email and password are required for two-factor authentication" - ) - ); - } - const [res] = await db - .select() - .from(users) - .where( - and( - eq(users.type, UserType.Internal), - eq(users.email, email) - ) - ); - user = res; - - const validPassword = await verifyPassword( - password, - user.passwordHash! - ); - if (!validPassword) { - return next(unauthorized()); - } - } - - if (!user) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Username or password incorrect. Email: ${email}. IP: ${req.ip}.` - ); - } - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - "Username or password is incorrect" - ) - ); - } - - if (user.type !== UserType.Internal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Two-factor authentication is not supported for external users" - ) - ); - } - - if (user.twoFactorEnabled) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Two-factor authentication is already enabled" - ) - ); - } - - if (!user.twoFactorSecret) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "User has not requested two-factor authentication" - ) - ); - } - const valid = await verifyTotpCode( code, user.twoFactorSecret, @@ -138,9 +89,7 @@ export async function verifyTotp( await db.transaction(async (trx) => { await trx .update(users) - .set({ - twoFactorEnabled: true - }) + .set({ twoFactorEnabled: true }) .where(eq(users.userId, user.userId)); const backupCodes = await generateBackupCodes(); @@ -204,3 +153,12 @@ export async function verifyTotp( ); } } + +async function generateBackupCodes(): Promise { + const codes = []; + for (let i = 0; i < 10; i++) { + const code = generateRandomString(6, alphabet("0-9", "A-Z", "a-z")); + codes.push(code); + } + return codes; +} diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index b4b2deea..a9208423 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -4,8 +4,8 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -import { resourceAccessToken, resources, sessions } from "@server/db"; -import { db } from "@server/db"; +import { resourceAccessToken, resources, sessions } from "@server/db/schemas"; +import db from "@server/db"; import { eq } from "drizzle-orm"; import { createResourceSession, @@ -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/response"; +import { response } from "@server/lib"; const exchangeSessionBodySchema = z.object({ requestToken: z.string(), @@ -52,26 +52,20 @@ export async function exchangeSession( try { const { requestToken, host, requestIp } = parsedBody.data; - let cleanHost = host; - // if the host ends with :port - if (cleanHost.match(/:[0-9]{1,5}$/)) { - const matched = ''+cleanHost.match(/:[0-9]{1,5}$/); - cleanHost = cleanHost.slice(0, -1*matched.length); - } const clientIp = requestIp?.split(":")[0]; const [resource] = await db .select() .from(resources) - .where(eq(resources.fullDomain, cleanHost)) + .where(eq(resources.fullDomain, host)) .limit(1); if (!resource) { return next( createHttpError( HttpCode.NOT_FOUND, - `Resource with host ${cleanHost} not found` + `Resource with host ${host} not found` ) ); } diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index b0ad9873..0a459dcf 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,136 +1,61 @@ +import { isPathAllowed } from './verifySession'; import { assertEquals } from '@test/assert'; -function isPathAllowed(pattern: string, path: string): boolean { - - // Normalize and split paths into segments - const normalize = (p: string) => p.split("/").filter(Boolean); - const patternParts = normalize(pattern); - const pathParts = normalize(path); - - - // Recursive function to try different wildcard matches - function matchSegments(patternIndex: number, pathIndex: number): boolean { - const indent = " ".repeat(pathIndex); // Indent based on recursion depth - const currentPatternPart = patternParts[patternIndex]; - const currentPathPart = pathParts[pathIndex]; - - // If we've consumed all pattern parts, we should have consumed all path parts - if (patternIndex >= patternParts.length) { - const result = pathIndex >= pathParts.length; - return result; - } - - // If we've consumed all path parts but still have pattern parts - if (pathIndex >= pathParts.length) { - // The only way this can match is if all remaining pattern parts are wildcards - const remainingPattern = patternParts.slice(patternIndex); - const result = remainingPattern.every((p) => p === "*"); - return result; - } - - // For full segment wildcards, try consuming different numbers of path segments - if (currentPatternPart === "*") { - - // Try consuming 0 segments (skip the wildcard) - if (matchSegments(patternIndex + 1, pathIndex)) { - return true; - } - - // Try consuming current segment and recursively try rest - if (matchSegments(patternIndex, pathIndex + 1)) { - return true; - } - - return false; - } - - // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix") - if (currentPatternPart.includes("*")) { - // Convert the pattern segment to a regex pattern - const regexPattern = currentPatternPart - .replace(/\*/g, ".*") // Replace * with .* for regex wildcard - .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed - - const regex = new RegExp(`^${regexPattern}$`); - - if (regex.test(currentPathPart)) { - return matchSegments(patternIndex + 1, pathIndex + 1); - } - - return false; - } - - // For regular segments, they must match exactly - if (currentPatternPart !== currentPathPart) { - return false; - } - - // Move to next segments in both pattern and path - return matchSegments(patternIndex + 1, pathIndex + 1); - } - - const result = matchSegments(0, 0); - return result; -} - function runTests() { console.log('Running path matching tests...'); - + // Test exact matching assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed'); assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); - + // Test with leading and trailing slashes assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match'); assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match'); assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match'); - + // Test simple wildcard matching assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match'); assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match'); - + // Test multiple wildcards assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments'); assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match'); - + // Test wildcard consumption behavior assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); - + // Test complex nested paths assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match'); assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match'); - + // Test for the requested padbootstrap* pattern assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)'); - + // Test wildcard edge cases assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments'); - + // Test patterns with partial segment matches assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard'); assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard'); - assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path'); - assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path'); - console.log('All tests passed!'); } @@ -139,4 +64,4 @@ try { runTests(); } catch (error) { console.error('Test failed:', error); -} +} \ No newline at end of file diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 87eeac97..0c2e6493 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -5,35 +5,34 @@ import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; +import db from "@server/db"; import { - getResourceByDomain, - getUserOrgRoles, - getResourceRules, - getRoleResourceAccess, - getUserResourceAccess, - getUserSessionWithUser -} from "@server/db/queries/verifySessionQueries"; -import { - LoginPage, Resource, ResourceAccessToken, - ResourceHeaderAuth, ResourcePassword, + resourcePassword, ResourcePincode, - ResourceRule -} from "@server/db"; + resourcePincode, + ResourceRule, + resourceRules, + resources, + roleResources, + sessions, + userOrgs, + userResources, + users +} from "@server/db/schemas"; import config from "@server/lib/config"; import { isIpInCidr } from "@server/lib/ip"; import { response } from "@server/lib/response"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import NodeCache from "node-cache"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -import { getCountryCodeForIp } from "@server/lib/geoip"; -import { verifyPassword } from "@server/auth/password"; // We'll see if this speeds anything up const cache = new NodeCache({ @@ -57,16 +56,9 @@ export type VerifyResourceSessionSchema = z.infer< typeof verifyResourceSessionSchema >; -type BasicUserData = { - username: string; - email: string | null; - name: string | null; -}; - export type VerifyUserResponse = { valid: boolean; redirectUrl?: string; - userData?: BasicUserData; }; export async function verifyResourceSession( @@ -98,37 +90,14 @@ 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 }); - if (requestIp.startsWith("[") && requestIp.includes("]")) { - // if brackets are found, extract the IPv6 address from between the brackets - const ipv6Match = requestIp.match(/\[(.*?)\]/); - if (ipv6Match) { - return ipv6Match[1]; - } - } - - // ivp4 - // split at last colon - const lastColonIndex = requestIp.lastIndexOf(":"); - if (lastColonIndex !== -1) { - return requestIp.substring(0, lastColonIndex); - } - return requestIp; - })() - : undefined; - - logger.debug("Client IP:", { clientIp }); + const clientIp = requestIp?.split(":")[0]; let cleanHost = host; - // if the host ends with :port, strip it - if (cleanHost.match(/:[0-9]{1,5}$/)) { - const matched = "" + cleanHost.match(/:[0-9]{1,5}$/); - cleanHost = cleanHost.slice(0, -1 * matched.length); + // if the host ends with :443 or :80 remove it + if (cleanHost.endsWith(":443")) { + cleanHost = cleanHost.slice(0, -4); + } else if (cleanHost.endsWith(":80")) { + cleanHost = cleanHost.slice(0, -3); } const resourceCacheKey = `resource:${cleanHost}`; @@ -137,26 +106,42 @@ export async function verifyResourceSession( resource: Resource | null; pincode: ResourcePincode | null; password: ResourcePassword | null; - headerAuth: ResourceHeaderAuth | null; } | undefined = cache.get(resourceCacheKey); if (!resourceData) { - const result = await getResourceByDomain(cleanHost); + const [result] = await db + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .where(eq(resources.fullDomain, cleanHost)) + .limit(1); if (!result) { - logger.debug(`Resource not found ${cleanHost}`); + logger.debug("Resource not found", cleanHost); return notAllowed(res); } - resourceData = result; + resourceData = { + resource: result.resources, + pincode: result.resourcePincode, + password: result.resourcePassword + }; + cache.set(resourceCacheKey, resourceData); } - const { resource, pincode, password, headerAuth } = resourceData; + const { resource, pincode, password } = resourceData; if (!resource) { - logger.debug(`Resource not found ${cleanHost}`); + logger.debug("Resource not found", cleanHost); return notAllowed(res); } @@ -181,30 +166,23 @@ export async function verifyResourceSession( } else if (action == "DROP") { logger.debug("Resource denied by rule"); return notAllowed(res); - } else if (action == "PASS") { - logger.debug( - "Resource passed by rule, continuing to auth checks" - ); - // Continue to authentication checks below } // 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 ( - !sso && + !resource.sso && !pincode && !password && - !resource.emailWhitelistEnabled && - !headerAuth + !resource.emailWhitelistEnabled ) { logger.debug("Resource allowed because no auth"); return allowed(res); } - const redirectPath = `/auth/resource/${encodeURIComponent( - resource.resourceGuid + const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent( + resource.resourceId )}?redirect=${encodeURIComponent(originalRequestURL)}`; // check for access token in headers @@ -291,46 +269,6 @@ 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( @@ -412,26 +350,23 @@ export async function verifyResourceSession( resourceSession.userSessionId }:${resource.resourceId}`; - let allowedUserData: BasicUserData | null | undefined = + let isAllowed: boolean | undefined = cache.get(userAccessCacheKey); - if (allowedUserData === undefined) { - allowedUserData = await isUserAllowedToAccessResource( + if (isAllowed === undefined) { + isAllowed = await isUserAllowedToAccessResource( resourceSession.userSessionId, resource ); - cache.set(userAccessCacheKey, allowedUserData); + cache.set(userAccessCacheKey, isAllowed); } - if ( - allowedUserData !== null && - allowedUserData !== undefined - ) { + if (isAllowed) { logger.debug( "Resource allowed because user session is valid" ); - return allowed(res, allowedUserData); + return allowed(res); } } } @@ -446,10 +381,7 @@ export async function verifyResourceSession( }. IP: ${clientIp}.` ); } - - logger.debug(`Redirecting to login at ${redirectPath}`); - - return notAllowed(res, redirectPath, resource.orgId); + return notAllowed(res, redirectUrl); } catch (e) { console.error(e); return next( @@ -504,34 +436,7 @@ function extractResourceSessionToken( return latest.token; } -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}`; - } - +function notAllowed(res: Response, redirectUrl?: string) { const data = { data: { valid: false, redirectUrl }, success: true, @@ -543,85 +448,138 @@ async function notAllowed( return response(res, data); } -function allowed(res: Response, userData?: BasicUserData) { +function allowed(res: Response) { const data = { - data: - userData !== undefined && userData !== null - ? { valid: true, ...userData } - : { valid: true }, + data: { valid: true }, success: true, error: false, message: "Access allowed", status: HttpCode.OK }; + logger.debug(JSON.stringify(data)); 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 -): Promise { - const result = await getUserSessionWithUser(userSessionId); +): Promise { + const [res] = await db + .select() + .from(sessions) + .leftJoin(users, eq(users.userId, sessions.userId)) + .where(eq(sessions.sessionId, userSessionId)); - if (!result) { - return null; - } - - const { user, session } = result; + const user = res.user; + const session = res.session; if (!user || !session) { - return null; + return false; } if ( config.getRawConfig().flags?.require_email_verification && !user.emailVerified ) { - return null; + return false; } - const userOrgRoles = await getUserOrgRoles(user.userId, resource.orgId); + const userOrgRole = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, user.userId), + eq(userOrgs.orgId, resource.orgId) + ) + ) + .limit(1); - const roleResourceAccess = await getRoleResourceAccess( - resource.resourceId, - userOrgRoles - ); - - if (roleResourceAccess) { - return { - username: user.username, - email: user.email, - name: user.name - }; + if (userOrgRole.length === 0) { + return false; } - const userResourceAccess = await getUserResourceAccess( - user.userId, - resource.resourceId - ); + const roleResourceAccess = await db + .select() + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resource.resourceId), + eq(roleResources.roleId, userOrgRole[0].roleId) + ) + ) + .limit(1); - if (userResourceAccess) { - return { - username: user.username, - email: user.email, - name: user.name - }; + if (roleResourceAccess.length > 0) { + return true; } - return null; + const userResourceAccess = await db + .select() + .from(userResources) + .where( + and( + eq(userResources.userId, user.userId), + eq(userResources.resourceId, resource.resourceId) + ) + ) + .limit(1); + + if (userResourceAccess.length > 0) { + return true; + } + + return false; } async function checkRules( resourceId: number, clientIp: string | undefined, path: string | undefined -): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> { +): Promise<"ACCEPT" | "DROP" | undefined> { const ruleCacheKey = `rules:${resourceId}`; let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey); if (!rules) { - rules = await getResourceRules(resourceId); + rules = await db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)); + cache.set(ruleCacheKey, rules); } @@ -652,12 +610,6 @@ async function checkRules( isPathAllowed(rule.value, path) ) { return rule.action as any; - } else if ( - clientIp && - rule.match == "GEOIP" && - (await isIpInGeoIP(clientIp, rule.value)) - ) { - return rule.action as any; } } @@ -782,48 +734,3 @@ export function isPathAllowed(pattern: string, path: string): boolean { logger.debug(`Final result: ${result}`); return result; } - -async function isIpInGeoIP(ip: string, countryCode: string): Promise { - if (countryCode == "ALL") { - return true; - } - - const geoIpCacheKey = `geoip:${ip}`; - - let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); - - if (!cachedCountryCode) { - cachedCountryCode = await getCountryCodeForIp(ip); // do it locally - // Cache for longer since IP geolocation doesn't change frequently - cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes - } - - logger.debug(`IP ${ip} is in country: ${cachedCountryCode}`); - - 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 deleted file mode 100644 index e160e644..00000000 --- a/server/routers/certificates/createCertificate.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 80136de8..00000000 --- a/server/routers/certificates/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/createClient.ts b/server/routers/client/createClient.ts deleted file mode 100644 index 2dba9268..00000000 --- a/server/routers/client/createClient.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { - roles, - Client, - clients, - roleClients, - userClients, - olms, - clientSites, - exitNodes, - orgs, - sites -} from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import moment from "moment"; -import { hashPassword } from "@server/auth/password"; -import { isValidCIDR, isValidIP } from "@server/lib/validators"; -import { isIpInCidr } from "@server/lib/ip"; -import { OpenAPITags, registry } from "@server/openApi"; -import { listExitNodes } from "@server/lib/exitNodes"; - -const createClientParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const createClientSchema = z - .object({ - name: z.string().min(1).max(255), - siteIds: z.array(z.number().int().positive()), - olmId: z.string(), - secret: z.string(), - subnet: z.string(), - type: z.enum(["olm"]) - }) - .strict(); - -export type CreateClientBody = z.infer; - -export type CreateClientResponse = Client; - -registry.registerPath({ - method: "put", - path: "/org/{orgId}/client", - description: "Create a new client.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: createClientParamsSchema, - body: { - content: { - "application/json": { - schema: createClientSchema - } - } - } - }, - responses: {} -}); - -export async function createClient( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = createClientSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { name, type, siteIds, olmId, secret, subnet } = parsedBody.data; - - const parsedParams = createClientParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - - if (req.user && (!req.userRoleIds || req.userRoleIds.length === 0)) { - return next( - createHttpError(HttpCode.FORBIDDEN, "User does not have a role") - ); - } - - if (!isValidIP(subnet)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid subnet format. Please provide a valid CIDR notation." - ) - ); - } - - const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); - - if (!org) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Organization with ID ${orgId} not found` - ) - ); - } - - if (!org.subnet) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Organization with ID ${orgId} has no subnet defined` - ) - ); - } - - if (!isIpInCidr(subnet, org.subnet)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "IP is not in the CIDR range of the subnet." - ) - ); - } - - const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org - - // make sure the subnet is unique - const subnetExistsClients = await db - .select() - .from(clients) - .where( - and(eq(clients.subnet, updatedSubnet), eq(clients.orgId, orgId)) - ) - .limit(1); - - if (subnetExistsClients.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - `Subnet ${updatedSubnet} already exists in clients` - ) - ); - } - - const subnetExistsSites = await db - .select() - .from(sites) - .where( - and(eq(sites.address, updatedSubnet), eq(sites.orgId, orgId)) - ) - .limit(1); - - if (subnetExistsSites.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - `Subnet ${updatedSubnet} already exists in sites` - ) - ); - } - - await db.transaction(async (trx) => { - // TODO: more intelligent way to pick the exit node - const exitNodesList = await listExitNodes(orgId); - const randomExitNode = - exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; - - const adminRole = await trx - .select() - .from(roles) - .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) - .limit(1); - - if (adminRole.length === 0) { - trx.rollback(); - return next( - createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) - ); - } - - const [newClient] = await trx - .insert(clients) - .values({ - exitNodeId: randomExitNode.exitNodeId, - orgId, - name, - subnet: updatedSubnet, - type - }) - .returning(); - - await trx.insert(roleClients).values({ - roleId: adminRole[0].roleId, - clientId: newClient.clientId - }); - - if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { - // make sure the user can access the site - trx.insert(userClients).values({ - userId: req.user?.userId!, - clientId: newClient.clientId - }); - } - - // Create site to client associations - if (siteIds && siteIds.length > 0) { - await trx.insert(clientSites).values( - siteIds.map((siteId) => ({ - clientId: newClient.clientId, - siteId - })) - ); - } - - const secretHash = await hashPassword(secret); - - await trx.insert(olms).values({ - olmId, - secretHash, - clientId: newClient.clientId, - dateCreated: moment().toISOString() - }); - - return response(res, { - data: newClient, - success: true, - error: false, - message: "Site created successfully", - status: HttpCode.CREATED - }); - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts deleted file mode 100644 index a7512574..00000000 --- a/server/routers/client/deleteClient.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { clients, clientSites } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const deleteClientSchema = z - .object({ - clientId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); - -registry.registerPath({ - method: "delete", - path: "/client/{clientId}", - description: "Delete a client by its client ID.", - tags: [OpenAPITags.Client], - request: { - params: deleteClientSchema - }, - responses: {} -}); - -export async function deleteClient( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = deleteClientSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { clientId } = parsedParams.data; - - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (!client) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Client with ID ${clientId} not found` - ) - ); - } - - await db.transaction(async (trx) => { - // Delete the client-site associations first - await trx - .delete(clientSites) - .where(eq(clientSites.clientId, clientId)); - - // Then delete the client itself - await trx - .delete(clients) - .where(eq(clients.clientId, clientId)); - }); - - return response(res, { - data: null, - success: true, - error: false, - message: "Client deleted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts deleted file mode 100644 index d362526f..00000000 --- a/server/routers/client/getClient.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { clients, clientSites } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import stoi from "@server/lib/stoi"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const getClientSchema = z - .object({ - clientId: z.string().transform(stoi).pipe(z.number().int().positive()) - }) - .strict(); - -async function query(clientId: number) { - // Get the client - const [client] = await db - .select() - .from(clients) - .where(and(eq(clients.clientId, clientId))) - .limit(1); - - if (!client) { - return null; - } - - // Get the siteIds associated with this client - const sites = await db - .select({ siteId: clientSites.siteId }) - .from(clientSites) - .where(eq(clientSites.clientId, clientId)); - - // Add the siteIds to the client object - return { - ...client, - siteIds: sites.map((site) => site.siteId) - }; -} - -export type GetClientResponse = NonNullable>>; - -registry.registerPath({ - method: "get", - path: "/client/{clientId}", - description: "Get a client by its client ID.", - tags: [OpenAPITags.Client], - request: { - params: getClientSchema - }, - responses: {} -}); - -export async function getClient( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = getClientSchema.safeParse(req.params); - if (!parsedParams.success) { - logger.error( - `Error parsing params: ${fromError(parsedParams.error).toString()}` - ); - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { clientId } = parsedParams.data; - - const client = await query(clientId); - - if (!client) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Client not found") - ); - } - - return response(res, { - data: client, - success: true, - error: false, - message: "Client retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/client/index.ts b/server/routers/client/index.ts deleted file mode 100644 index 385c7bed..00000000 --- a/server/routers/client/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./pickClientDefaults"; -export * from "./createClient"; -export * from "./deleteClient"; -export * from "./listClients"; -export * from "./updateClient"; -export * from "./getClient"; \ No newline at end of file diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts deleted file mode 100644 index df5e0a99..00000000 --- a/server/routers/client/listClients.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { db } from "@server/db"; -import { - clients, - orgs, - roleClients, - sites, - userClients, - clientSites -} from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { and, count, eq, inArray, or, sql } from "drizzle-orm"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const listClientsParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const listClientsSchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -function queryClients(orgId: string, accessibleClientIds: number[]) { - return db - .select({ - clientId: clients.clientId, - orgId: clients.orgId, - name: clients.name, - pubKey: clients.pubKey, - subnet: clients.subnet, - megabytesIn: clients.megabytesIn, - megabytesOut: clients.megabytesOut, - orgName: orgs.name, - type: clients.type, - online: clients.online - }) - .from(clients) - .leftJoin(orgs, eq(clients.orgId, orgs.orgId)) - .where( - and( - inArray(clients.clientId, accessibleClientIds), - eq(clients.orgId, orgId) - ) - ); -} - -async function getSiteAssociations(clientIds: number[]) { - if (clientIds.length === 0) return []; - - return db - .select({ - clientId: clientSites.clientId, - siteId: clientSites.siteId, - siteName: sites.name, - siteNiceId: sites.niceId - }) - .from(clientSites) - .leftJoin(sites, eq(clientSites.siteId, sites.siteId)) - .where(inArray(clientSites.clientId, clientIds)); -} - -export type ListClientsResponse = { - clients: Array>[0] & { sites: Array<{ - siteId: number; - siteName: string | null; - siteNiceId: string | null; - }> }>; - pagination: { total: number; limit: number; offset: number }; -}; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/clients", - description: "List all clients for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - query: listClientsSchema, - params: listClientsParamsSchema - }, - responses: {} -}); - -export async function listClients( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedQuery = listClientsSchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error) - ) - ); - } - const { limit, offset } = parsedQuery.data; - - const parsedParams = listClientsParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error) - ) - ); - } - const { orgId } = parsedParams.data; - - if (req.user && orgId && orgId !== req.userOrgId) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) - ); - } - - let accessibleClients; - if (req.user) { - accessibleClients = await db - .select({ - clientId: sql`COALESCE(${userClients.clientId}, ${roleClients.clientId})` - }) - .from(userClients) - .fullJoin( - roleClients, - eq(userClients.clientId, roleClients.clientId) - ) - .where( - or( - eq(userClients.userId, req.user!.userId), - inArray(roleClients.roleId, req.userRoleIds!) - ) - ); - } else { - accessibleClients = await db - .select({ clientId: clients.clientId }) - .from(clients) - .where(eq(clients.orgId, orgId)); - } - - const accessibleClientIds = accessibleClients.map( - (client) => client.clientId - ); - const baseQuery = queryClients(orgId, accessibleClientIds); - - // Get client count - const countQuery = db - .select({ count: count() }) - .from(clients) - .where( - and( - inArray(clients.clientId, accessibleClientIds), - eq(clients.orgId, orgId) - ) - ); - - const clientsList = await baseQuery.limit(limit).offset(offset); - const totalCountResult = await countQuery; - const totalCount = totalCountResult[0].count; - - // Get associated sites for all clients - const clientIds = clientsList.map(client => client.clientId); - const siteAssociations = await getSiteAssociations(clientIds); - - // Group site associations by client ID - const sitesByClient = siteAssociations.reduce((acc, association) => { - if (!acc[association.clientId]) { - acc[association.clientId] = []; - } - acc[association.clientId].push({ - siteId: association.siteId, - siteName: association.siteName, - siteNiceId: association.siteNiceId - }); - return acc; - }, {} as Record>); - - // Merge clients with their site associations - const clientsWithSites = clientsList.map(client => ({ - ...client, - sites: sitesByClient[client.clientId] || [] - })); - - return response(res, { - data: { - clients: clientsWithSites, - pagination: { - total: totalCount, - limit, - offset - } - }, - success: true, - error: false, - message: "Clients retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts deleted file mode 100644 index 6f452142..00000000 --- a/server/routers/client/pickClientDefaults.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { generateId } from "@server/auth/sessions/app"; -import { getNextAvailableClientSubnet } from "@server/lib/ip"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -export type PickClientDefaultsResponse = { - olmId: string; - olmSecret: string; - subnet: string; -}; - -const pickClientDefaultsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/pick-client-defaults", - description: "Return pre-requisite data for creating a client.", - tags: [OpenAPITags.Client, OpenAPITags.Site], - request: { - params: pickClientDefaultsSchema - }, - responses: {} -}); - -export async function pickClientDefaults( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = pickClientDefaultsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - - const olmId = generateId(15); - const secret = generateId(48); - - const newSubnet = await getNextAvailableClientSubnet(orgId); - if (!newSubnet) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "No available subnet found" - ) - ); - } - - const subnet = newSubnet.split("/")[0]; - - return response(res, { - data: { - olmId: olmId, - olmSecret: secret, - subnet: subnet - }, - success: true, - error: false, - message: "Organization retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts deleted file mode 100644 index 38a95945..00000000 --- a/server/routers/client/targets.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { sendToClient } from "@server/routers/ws"; - -export async function addTargets( - newtId: string, - destinationIp: string, - destinationPort: number, - protocol: string, - port: number -) { - const target = `${port}:${destinationIp}:${destinationPort}`; - - await sendToClient(newtId, { - type: `newt/wg/${protocol}/add`, - data: { - targets: [target] // We can only use one target for WireGuard right now - } - }); -} - -export async function removeTargets( - newtId: string, - destinationIp: string, - destinationPort: number, - protocol: string, - port: number -) { - const target = `${port}:${destinationIp}:${destinationPort}`; - - await sendToClient(newtId, { - type: `newt/wg/${protocol}/remove`, - data: { - targets: [target] // We can only use one target for WireGuard right now - } - }); -} diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts deleted file mode 100644 index 80050f6c..00000000 --- a/server/routers/client/updateClient.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -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"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; -import { - addPeer as newtAddPeer, - deletePeer as newtDeletePeer -} from "../newt/peers"; -import { - addPeer as olmAddPeer, - deletePeer as olmDeletePeer -} from "../olm/peers"; -import { sendToExitNode } from "@server/lib/exitNodes"; - -const updateClientParamsSchema = z - .object({ - clientId: z.string().transform(Number).pipe(z.number().int().positive()) - }) - .strict(); - -const updateClientSchema = z - .object({ - name: z.string().min(1).max(255).optional(), - siteIds: z - .array(z.number().int().positive()) - .optional() - }) - .strict(); - -export type UpdateClientBody = z.infer; - -registry.registerPath({ - method: "post", - path: "/client/{clientId}", - description: "Update a client by its client ID.", - tags: [OpenAPITags.Client], - request: { - params: updateClientParamsSchema, - body: { - content: { - "application/json": { - schema: updateClientSchema - } - } - } - }, - responses: {} -}); - -interface PeerDestination { - destinationIP: string; - destinationPort: number; -} - -export async function updateClient( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = updateClientSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { name, siteIds } = parsedBody.data; - - const parsedParams = updateClientParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { clientId } = parsedParams.data; - - // Fetch the client to make sure it exists and the user has access to it - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (!client) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Client with ID ${clientId} not found` - ) - ); - } - - let sitesAdded = []; - let sitesRemoved = []; - - // 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 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) { - await trx - .update(clients) - .set({ name }) - .where(eq(clients.clientId, clientId)); - } - - // Update site associations if provided - // Remove sites that are no longer associated - for (const siteId of sitesRemoved) { - await trx - .delete(clientSites) - .where( - and( - eq(clientSites.clientId, clientId), - eq(clientSites.siteId, siteId) - ) - ); - } - - // Add new site associations - for (const siteId of sitesAdded) { - await trx.insert(clientSites).values({ - clientId, - siteId - }); - } - - // Fetch the updated client - [updatedClient] = await trx - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - // 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); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts deleted file mode 100644 index e39c09d3..00000000 --- a/server/routers/domain/createOrgDomain.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, Domain, domains, OrgDomains, orgDomains } from "@server/db"; -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 { subdomainSchema } from "@server/lib/schemas"; -import { generateId } from "@server/auth/sessions/app"; -import { eq, and } from "drizzle-orm"; -import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; -import config from "@server/lib/config"; - -const paramsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const bodySchema = z - .object({ - type: z.enum(["ns", "cname", "wildcard"]), - baseDomain: subdomainSchema - }) - .strict(); - -export type CreateDomainResponse = { - domainId: string; - nsRecords?: string[]; - cnameRecords?: { baseDomain: string; value: string }[]; - aRecords?: { baseDomain: string; value: string }[]; - txtRecords?: { baseDomain: string; value: string }[]; -}; - -// Helper to check if a domain is a subdomain or equal to another domain -function isSubdomainOrEqual(a: string, b: string): boolean { - const aParts = a.toLowerCase().split("."); - const bParts = b.toLowerCase().split("."); - if (aParts.length < bParts.length) return false; - return aParts.slice(-bParts.length).join(".") === bParts.join("."); -} - -export async function createOrgDomain( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - const { type, baseDomain } = parsedBody.data; - - 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)) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid domain format") - ); - } - - /* 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"]; - let txtRecords: CreateDomainResponse["txtRecords"]; - let nsRecords: CreateDomainResponse["nsRecords"]; - let returned: Domain | undefined; - - await db.transaction(async (trx) => { - const [existing] = await trx - .select() - .from(domains) - .where( - and( - eq(domains.baseDomain, baseDomain), - eq(domains.type, type) - ) - ) - .leftJoin( - orgDomains, - eq(orgDomains.domainId, domains.domainId) - ); - - if (existing) { - const { - domains: existingDomain, - orgDomains: existingOrgDomain - } = existing; - - // user alrady added domain to this account - // always reject - if (existingOrgDomain?.orgId === orgId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Domain is already added to this org" - ) - ); - } - - // domain already exists elsewhere - // check if it's already fully verified - if (existingDomain.verified) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Domain is already verified to an org" - ) - ); - } - } - - // --- Domain overlap logic --- - // Only consider existing verified domains - const verifiedDomains = await trx - .select() - .from(domains) - .where(eq(domains.verified, true)); - - /* if (type == "cname") { - // Block if a verified CNAME exists at the same name - const cnameExists = verifiedDomains.some( - (d) => d.type === "cname" && d.baseDomain === baseDomain - ); - if (cnameExists) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `A CNAME record already exists for ${baseDomain}. Only one CNAME record is allowed per domain.` - ) - ); - } - // Block if a verified NS exists at or below (same or subdomain) - const nsAtOrBelow = verifiedDomains.some( - (d) => - d.type === "ns" && - (isSubdomainOrEqual(baseDomain, d.baseDomain) || - baseDomain === d.baseDomain) - ); - if (nsAtOrBelow) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `A nameserver (NS) record exists at or below ${baseDomain}. You cannot create a CNAME record here.` - ) - ); - } - } else if (type == "ns") { - // Block if a verified NS exists at or below (same or subdomain) - const nsAtOrBelow = verifiedDomains.some( - (d) => - d.type === "ns" && - (isSubdomainOrEqual(baseDomain, d.baseDomain) || - baseDomain === d.baseDomain) - ); - if (nsAtOrBelow) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `A nameserver (NS) record already exists at or below ${baseDomain}. You cannot create another NS record here.` - ) - ); - } - } else if (type == "wildcard") { - // TODO: Figure out how to handle wildcards - } - */ - - const domainId = generateId(15); - - const [insertedDomain] = await trx - .insert(domains) - .values({ - domainId, - baseDomain, - type, - verified: type === "wildcard" ? true : false - }) - .returning(); - - returned = insertedDomain; - - // add domain to account - await trx - .insert(orgDomains) - .values({ - orgId, - domainId - }) - .returning(); - - // TODO: This needs to be cross region and not hardcoded - /* if (type === "ns") { - nsRecords = config.getRawConfig().dns.nameservers as string[]; - } else if (type === "cname") { - cnameRecords = [ - { - value: `${domainId}.${config.getRawConfig().dns.cname_extension}`, - baseDomain: baseDomain - }, - { - value: `_acme-challenge.${domainId}.${config.getRawConfig().dns.cname_extension}`, - baseDomain: `_acme-challenge.${baseDomain}` - } - ]; - } */ - if (type === "wildcard") { - aRecords = [ - { - value: `Server IP Address`, - baseDomain: `*.${baseDomain}` - }, - { - value: `Server IP Address`, - baseDomain: `${baseDomain}` - } - ]; - } - - numOrgDomains = await trx - .select() - .from(orgDomains) - .where(eq(orgDomains.orgId, orgId)); - }); - - if (!returned) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create domain" - ) - ); - } - - return response(res, { - data: { - domainId: returned.domainId, - cnameRecords, - txtRecords, - nsRecords, - aRecords - }, - success: true, - error: false, - message: "Domain created successfully", - status: HttpCode.CREATED - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts deleted file mode 100644 index 345dafe7..00000000 --- a/server/routers/domain/deleteOrgDomain.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, domains, OrgDomains, orgDomains } from "@server/db"; -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 { and, eq } from "drizzle-orm"; - -const paramsSchema = z - .object({ - domainId: z.string(), - orgId: z.string() - }) - .strict(); - -export type DeleteAccountDomainResponse = { - success: boolean; -}; - -export async function deleteAccountDomain( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsed = paramsSchema.safeParse(req.params); - if (!parsed.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsed.error).toString() - ) - ); - } - const { domainId, orgId } = parsed.data; - - let numOrgDomains: OrgDomains[] | undefined; - - await db.transaction(async (trx) => { - const [existing] = await trx - .select() - .from(orgDomains) - .where( - and( - eq(orgDomains.orgId, orgId), - eq(orgDomains.domainId, domainId) - ) - ) - .innerJoin( - domains, - eq(orgDomains.domainId, domains.domainId) - ); - - if (!existing) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Domain not found for this account" - ) - ); - } - - if (existing.domains.configManaged) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Cannot delete a domain that is managed by the config" - ) - ); - } - - await trx - .delete(orgDomains) - .where( - and( - eq(orgDomains.orgId, orgId), - eq(orgDomains.domainId, domainId) - ) - ); - - await trx.delete(domains).where(eq(domains.domainId, domainId)); - - numOrgDomains = await trx - .select() - .from(orgDomains) - .where(eq(orgDomains.orgId, orgId)); - }); - - return response(res, { - data: { success: true }, - success: true, - error: false, - message: "Domain deleted from account successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index c0cafafe..2233b069 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -1,4 +1 @@ export * from "./listDomains"; -export * from "./createOrgDomain"; -export * from "./deleteOrgDomain"; -export * from "./restartOrgDomain"; \ No newline at end of file diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index fe51cde6..c525e1d8 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { domains, orgDomains, users } from "@server/db"; +import { domains, orgDomains, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -37,12 +37,7 @@ async function queryDomains(orgId: string, limit: number, offset: number) { const res = await db .select({ domainId: domains.domainId, - baseDomain: domains.baseDomain, - verified: domains.verified, - type: domains.type, - failed: domains.failed, - tries: domains.tries, - configManaged: domains.configManaged + baseDomain: domains.baseDomain }) .from(orgDomains) .where(eq(orgDomains.orgId, orgId)) @@ -117,7 +112,7 @@ export async function listDomains( }, success: true, error: false, - message: "Domains retrieved successfully", + message: "Users retrieved successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/domain/restartOrgDomain.ts b/server/routers/domain/restartOrgDomain.ts deleted file mode 100644 index f40f2516..00000000 --- a/server/routers/domain/restartOrgDomain.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, domains } from "@server/db"; -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 { and, eq } from "drizzle-orm"; - -const paramsSchema = z - .object({ - domainId: z.string(), - orgId: z.string() - }) - .strict(); - -export type RestartOrgDomainResponse = { - success: boolean; -}; - -export async function restartOrgDomain( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsed = paramsSchema.safeParse(req.params); - if (!parsed.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsed.error).toString() - ) - ); - } - const { domainId, orgId } = parsed.data; - - await db - .update(domains) - .set({ failed: false, tries: 0 }) - .where(and(eq(domains.domainId, domainId))); - - return response(res, { - data: { success: true }, - success: true, - error: false, - message: "Domain restarted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/domain/types.ts b/server/routers/domain/types.ts deleted file mode 100644 index 4ae48fb1..00000000 --- a/server/routers/domain/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 c161bef5..96f569b3 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -8,14 +8,13 @@ import * as target from "./target"; import * as user from "./user"; import * as auth from "./auth"; import * as role from "./role"; -import * as client from "./client"; -import * as siteResource from "./siteResource"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; -import * as apiKeys from "./apiKeys"; +// import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, + rateLimitMiddleware, verifySessionMiddleware, verifySessionUserMiddleware, verifyOrgAccess, @@ -27,21 +26,15 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, - verifyIsLoggedInUser, - verifyClientAccess, - verifyApiKeyAccess, - verifyDomainAccess, - verifyClientsEnabled, - verifyUserHasAction, - verifyUserIsOrgOwner, - verifySiteResourceAccess + verifyIsLoggedInUser + // verifyApiKeyAccess } from "@server/middlewares"; +import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; -import { createNewt, getNewtToken } from "./newt"; -import { getOlmToken } from "./olm"; -import rateLimit, { ipKeyGenerator } from "express-rate-limit"; +import { verifyUserIsOrgOwner } from "../middlewares/verifyUserIsOrgOwner"; +import { createNewt, getToken } from "./newt"; +import rateLimit from "express-rate-limit"; import createHttpError from "http-errors"; -import { createStore } from "@server/lib/rateLimitStore"; // Root routes export const unauthenticated = Router(); @@ -54,7 +47,6 @@ unauthenticated.get("/", (_, res) => { export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); -authenticated.get("/pick-org-defaults", org.pickOrgDefaults); authenticated.get("/org/checkId", org.checkId); authenticated.put("/org", getUserOrgs, org.createOrg); @@ -73,12 +65,10 @@ authenticated.post( verifyUserHasAction(ActionsEnum.updateOrg), org.updateOrg ); - authenticated.delete( "/org/:orgId", verifyOrgAccess, verifyUserIsOrgOwner, - verifyUserHasAction(ActionsEnum.deleteOrg), org.deleteOrg ); @@ -113,55 +103,6 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getSite), site.getSite ); - -authenticated.get( - "/org/:orgId/pick-client-defaults", - verifyClientsEnabled, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createClient), - client.pickClientDefaults -); - -authenticated.get( - "/org/:orgId/clients", - verifyClientsEnabled, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.listClients), - client.listClients -); - -authenticated.get( - "/client/:clientId", - verifyClientsEnabled, - verifyClientAccess, - verifyUserHasAction(ActionsEnum.getClient), - client.getClient -); - -authenticated.put( - "/org/:orgId/client", - verifyClientsEnabled, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createClient), - client.createClient -); - -authenticated.delete( - "/client/:clientId", - verifyClientsEnabled, - verifyClientAccess, - verifyUserHasAction(ActionsEnum.deleteClient), - client.deleteClient -); - -authenticated.post( - "/client/:clientId", - verifyClientsEnabled, - verifyClientAccess, // this will check if the user has access to the client - verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client - client.updateClient -); - // authenticated.get( // "/site/:siteId/roles", // verifySiteAccess, @@ -181,91 +122,9 @@ authenticated.delete( site.deleteSite ); -authenticated.get( - "/site/:siteId/docker/status", - verifySiteAccess, - verifyUserHasAction(ActionsEnum.getSite), - site.dockerStatus -); -authenticated.get( - "/site/:siteId/docker/online", - verifySiteAccess, - verifyUserHasAction(ActionsEnum.getSite), - site.dockerOnline -); -authenticated.post( - "/site/:siteId/docker/check", - verifySiteAccess, - verifyUserHasAction(ActionsEnum.getSite), - site.checkDockerSocket -); -authenticated.post( - "/site/:siteId/docker/trigger", - verifySiteAccess, - verifyUserHasAction(ActionsEnum.getSite), - site.triggerFetchContainers -); -authenticated.get( - "/site/:siteId/docker/containers", - verifySiteAccess, - verifyUserHasAction(ActionsEnum.getSite), - site.listContainers -); - -// Site Resource endpoints authenticated.put( "/org/:orgId/site/:siteId/resource", verifyOrgAccess, - verifySiteAccess, - verifyUserHasAction(ActionsEnum.createSiteResource), - siteResource.createSiteResource -); - -authenticated.get( - "/org/:orgId/site/:siteId/resources", - verifyOrgAccess, - verifySiteAccess, - verifyUserHasAction(ActionsEnum.listSiteResources), - siteResource.listSiteResources -); - -authenticated.get( - "/org/:orgId/site-resources", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.listSiteResources), - siteResource.listAllSiteResourcesByOrg -); - -authenticated.get( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, - verifySiteResourceAccess, - verifyUserHasAction(ActionsEnum.getSiteResource), - siteResource.getSiteResource -); - -authenticated.post( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, - verifySiteResourceAccess, - verifyUserHasAction(ActionsEnum.updateSiteResource), - siteResource.updateSiteResource -); - -authenticated.delete( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, - verifySiteResourceAccess, - verifyUserHasAction(ActionsEnum.deleteSiteResource), - siteResource.deleteSiteResource -); - -authenticated.put( - "/org/:orgId/resource", - verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), resource.createResource ); @@ -283,12 +142,6 @@ authenticated.get( resource.listResources ); -authenticated.get( - "/org/:orgId/user-resources", - verifyOrgAccess, - resource.getUserResources -); - authenticated.get( "/org/:orgId/domains", verifyOrgAccess, @@ -338,12 +191,6 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getResource), resource.getResource ); -authenticated.get( - "/org/:orgId/resource/:niceId", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.getResource), - resource.getResource -); authenticated.post( "/resource/:resourceId", verifyResourceAccess, @@ -461,6 +308,28 @@ authenticated.post( user.addUserRole ); +// authenticated.put( +// "/role/:roleId/site", +// verifyRoleAccess, +// verifyUserInRole, +// verifyUserHasAction(ActionsEnum.addRoleSite), +// role.addRoleSite +// ); +// authenticated.delete( +// "/role/:roleId/site", +// verifyRoleAccess, +// verifyUserInRole, +// verifyUserHasAction(ActionsEnum.removeRoleSite), +// role.removeRoleSite +// ); +// authenticated.get( +// "/role/:roleId/sites", +// verifyRoleAccess, +// verifyUserInRole, +// verifyUserHasAction(ActionsEnum.listRoleSites), +// role.listRoleSites +// ); + authenticated.post( "/resource/:resourceId/roles", verifyResourceAccess, @@ -491,13 +360,6 @@ authenticated.post( resource.setResourcePincode ); -authenticated.post( - `/resource/:resourceId/header-auth`, - verifyResourceAccess, - verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), - resource.setResourceHeaderAuth -); - authenticated.post( `/resource/:resourceId/whitelist`, verifyResourceAccess, @@ -512,6 +374,13 @@ authenticated.get( resource.getResourceWhitelist ); +authenticated.post( + `/resource/:resourceId/transfer`, + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.updateResource), + resource.transferResource +); + authenticated.post( `/resource/:resourceId/access-token`, verifyResourceAccess, @@ -542,10 +411,7 @@ authenticated.get( authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); -unauthenticated.get( - "/resource/:resourceGuid/auth", - resource.getResourceAuthInfo -); +unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); // authenticated.get( // "/role/:roleId/resources", @@ -579,7 +445,6 @@ unauthenticated.get( unauthenticated.get("/user", verifySessionMiddleware, user.getUser); authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers); -authenticated.get("/user/:userId", verifyUserIsServerAdmin, user.adminGetUser); authenticated.delete( "/user/:userId", verifyUserIsServerAdmin, @@ -593,22 +458,8 @@ authenticated.put( user.createOrgUser ); -authenticated.post( - "/org/:orgId/user/:userId", - verifyOrgAccess, - verifyUserAccess, - verifyUserHasAction(ActionsEnum.updateOrgUser), - user.updateOrgUser -); - authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); -authenticated.post( - "/user/:userId/2fa", - verifyUserIsServerAdmin, - user.updateUser2FA -); - authenticated.get( "/org/:orgId/users", verifyOrgAccess, @@ -671,10 +522,10 @@ authenticated.post( idp.updateOidcIdp ); - - authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); +authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps); + authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); authenticated.put( @@ -701,10 +552,10 @@ 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); +/* authenticated.get( `/api-key/:apiKeyId`, verifyUserIsServerAdmin, @@ -786,187 +637,48 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getApiKey), apiKeys.getApiKey ); - -authenticated.put( - `/org/:orgId/domain`, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.createOrgDomain), - domain.createOrgDomain -); - -authenticated.post( - `/org/:orgId/domain/:domainId/restart`, - verifyOrgAccess, - verifyDomainAccess, - verifyUserHasAction(ActionsEnum.restartOrgDomain), - domain.restartOrgDomain -); - -authenticated.delete( - `/org/:orgId/domain/:domainId`, - verifyOrgAccess, - verifyDomainAccess, - verifyUserHasAction(ActionsEnum.deleteOrgDomain), - domain.deleteAccountDomain -); +*/ // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); 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}`, - 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)); - }, - store: createStore() + rateLimitMiddleware({ + windowMin: + config.getRawConfig().rate_limits.auth?.window_minutes || + config.getRawConfig().rate_limits.global.window_minutes, + max: + config.getRawConfig().rate_limits.auth?.max_requests || + config.getRawConfig().rate_limits.global.max_requests, + type: "IP_AND_PATH" }) ); -authRouter.put( - "/signup", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - 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)); - }, - store: createStore() - }), - auth.signup -); -authRouter.post( - "/login", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - 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)); - }, - store: createStore() - }), - auth.login -); +authRouter.put("/signup", auth.signup); +authRouter.post("/login", auth.login); authRouter.post("/logout", auth.logout); -authRouter.post( - "/newt/get-token", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 900, - 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)); - }, - store: createStore() - }), - getNewtToken -); -authRouter.post( - "/olm/get-token", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 900, - 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)); - }, - store: createStore() - }), - getOlmToken -); +authRouter.post("/newt/get-token", getToken); -authRouter.post( - "/2fa/enable", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => { - return `signup:${req.body.email || req.user?.userId || ipKeyGenerator(req.ip || "")}`; - }, - handler: (req, res, next) => { - const message = `You can only enable 2FA ${15} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), - auth.verifyTotp -); +authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp); authRouter.post( "/2fa/request", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => { - return `signup:${req.body.email || req.user?.userId || ipKeyGenerator(req.ip || "")}`; - }, - handler: (req, res, next) => { - const message = `You can only request a 2FA code ${15} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), + verifySessionUserMiddleware, auth.requestTotpSecret ); -authRouter.post( - "/2fa/disable", - verifySessionUserMiddleware, - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - 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)); - }, - store: createStore() - }), - auth.disable2fa -); -authRouter.post( - "/verify-email", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - 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)); - }, - store: createStore() - }), - verifySessionMiddleware, - auth.verifyEmail -); +authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa); +authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail); authRouter.post( "/verify-email/request", verifySessionMiddleware, rateLimit({ windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => - `requestEmailVerificationCode:${req.user?.email || ipKeyGenerator(req.ip || "")}`, + max: 3, + keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email}`, handler: (req, res, next) => { - const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`; + const message = `You can only request an email verification code ${3} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() + } }), auth.requestEmailVerificationCode ); @@ -981,77 +693,31 @@ authRouter.post( "/reset-password/request", rateLimit({ windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => - `requestPasswordReset:${req.body.email || ipKeyGenerator(req.ip || "")}`, + max: 3, + keyGenerator: (req) => `requestPasswordReset:${req.body.email}`, handler: (req, res, next) => { - const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`; + const message = `You can only request a password reset ${3} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() + } }), auth.requestPasswordReset ); -authRouter.post( - "/reset-password/", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - 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)); - }, - store: createStore() - }), - auth.resetPassword -); +authRouter.post("/reset-password/", auth.resetPassword); -authRouter.post( - "/resource/:resourceId/password", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => - `authWithPassword:${ipKeyGenerator(req.ip || "")}:${req.params.resourceId || ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only authenticate with password ${15} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), - resource.authWithPassword -); -authRouter.post( - "/resource/:resourceId/pincode", - rateLimit({ - windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => - `authWithPincode:${ipKeyGenerator(req.ip || "")}:${req.params.resourceId || ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only authenticate with pincode ${15} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), - resource.authWithPincode -); +authRouter.post("/resource/:resourceId/password", resource.authWithPassword); +authRouter.post("/resource/:resourceId/pincode", resource.authWithPincode); authRouter.post( "/resource/:resourceId/whitelist", rateLimit({ windowMs: 15 * 60 * 1000, - max: 15, - keyGenerator: (req) => - `authWithWhitelist:${ipKeyGenerator(req.ip || "")}:${req.body.email}:${req.params.resourceId}`, + max: 10, + keyGenerator: (req) => `authWithWhitelist:${req.body.email}`, handler: (req, res, next) => { - const message = `You can only request an email OTP ${15} times every ${15} minutes. Please try again later.`; + const message = `You can only request an email OTP ${10} times every ${15} minutes. Please try again later.`; return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() + } }), resource.authWithWhitelist ); @@ -1066,68 +732,3 @@ authRouter.post("/access-token", resource.authWithAccessToken); authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl); authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); - -authRouter.put("/set-server-admin", auth.setServerAdmin); -authRouter.get("/initial-setup-complete", auth.initialSetupComplete); -authRouter.post("/validate-setup-token", auth.validateSetupToken); - -// Security Key routes -authRouter.post( - "/security-key/register/start", - verifySessionUserMiddleware, - rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // Allow 5 security key registrations per 15 minutes - keyGenerator: (req) => - `securityKeyRegister:${req.user?.userId || ipKeyGenerator(req.ip || "")}`, - handler: (req, res, next) => { - const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), - auth.startRegistration -); -authRouter.post( - "/security-key/register/verify", - verifySessionUserMiddleware, - auth.verifyRegistration -); -authRouter.post( - "/security-key/authenticate/start", - rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 10, // Allow 10 authentication attempts per 15 minutes per IP - keyGenerator: (req) => { - return `securityKeyAuth:${req.body.email || ipKeyGenerator(req.ip || "")}`; - }, - handler: (req, res, next) => { - const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`; - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - store: createStore() - }), - auth.startAuthentication -); -authRouter.post("/security-key/authenticate/verify", auth.verifyAuthentication); -authRouter.get( - "/security-key/list", - verifySessionUserMiddleware, - auth.listSecurityKeys -); -authRouter.delete( - "/security-key/:credentialId", - verifySessionUserMiddleware, - 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 || "")}`, - 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)); - }, - store: createStore() - }), - auth.deleteSecurityKey -); diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts deleted file mode 100644 index 4c5efed7..00000000 --- a/server/routers/generatedLicense/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 06af7e46..00000000 --- a/server/routers/gerbil/createExitNode.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { db, ExitNode, exitNodes } from "@server/db"; -import { getUniqueExitNodeEndpointName } from "@server/db/names"; -import config from "@server/lib/config"; -import { getNextAvailableSubnet } from "@server/lib/exitNodes"; -import logger from "@server/logger"; -import { eq } from "drizzle-orm"; - -export async function createExitNode(publicKey: string, reachableAt: string | undefined) { - // Fetch exit node - const [exitNodeQuery] = await db.select().from(exitNodes).limit(1); - let exitNode: ExitNode; - if (!exitNodeQuery) { - const address = await getNextAvailableSubnet(); - // TODO: eventually we will want to get the next available port so that we can multiple exit nodes - // const listenPort = await getNextAvailablePort(); - const listenPort = config.getRawConfig().gerbil.start_port; - let subEndpoint = ""; - if (config.getRawConfig().gerbil.use_subdomain) { - subEndpoint = await getUniqueExitNodeEndpointName(); - } - - const exitNodeName = - config.getRawConfig().gerbil.exit_node_name || - `Exit Node ${publicKey.slice(0, 8)}`; - - // create a new exit node - [exitNode] = await db - .insert(exitNodes) - .values({ - publicKey, - endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`, - address, - online: true, - listenPort, - reachableAt, - name: exitNodeName - }) - .returning() - .execute(); - - logger.info( - `Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}` - ); - } else { - // update the existing exit node - [exitNode] = await db - .update(exitNodes) - .set({ - reachableAt, - publicKey - }) - .where(eq(exitNodes.publicKey, publicKey)) - .returning(); - - logger.info(`Updated exit node with reachableAt to ${reachableAt}`); - } - - return exitNode; -} diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts deleted file mode 100644 index 6eaf87e2..00000000 --- a/server/routers/gerbil/getAllRelays.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { - clients, - exitNodes, - newts, - olms, - Site, - sites, - clientSites, - ExitNode -} from "@server/db"; -import { db } from "@server/db"; -import { eq } from "drizzle-orm"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; - -// Define Zod schema for request validation -const getAllRelaysSchema = z.object({ - publicKey: z.string().optional() -}); - -// Type for peer destination -interface PeerDestination { - destinationIP: string; - destinationPort: number; -} - -// Updated mappings type to support multiple destinations per endpoint -interface ProxyMapping { - destinations: PeerDestination[]; -} - -export async function getAllRelays( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - // Validate request parameters - const parsedParams = getAllRelaysSchema.safeParse(req.body); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { publicKey } = parsedParams.data; - - if (!publicKey) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "publicKey is required") - ); - } - - // Fetch exit node - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.publicKey, publicKey)); - if (!exitNode) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Exit node not found") - ); - } - - const mappings = await generateRelayMappings(exitNode); - - logger.debug( - `Returning mappings for ${Object.keys(mappings).length} endpoints` - ); - return res.status(HttpCode.OK).send({ mappings }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) - ); - } -} - -export async function generateRelayMappings(exitNode: ExitNode) { - // Fetch sites for this exit node - const sitesRes = await db - .select() - .from(sites) - .where(eq(sites.exitNodeId, exitNode.exitNodeId)); - - if (sitesRes.length === 0) { - return {}; - } - - // Initialize mappings object for multi-peer support - const mappings: { [key: string]: ProxyMapping } = {}; - - // Process each site - for (const site of sitesRes) { - if (!site.endpoint || !site.subnet || !site.listenPort) { - continue; - } - - // Find all clients associated with this site through clientSites - const clientSitesRes = await db - .select() - .from(clientSites) - .where(eq(clientSites.siteId, site.siteId)); - - for (const clientSite of clientSitesRes) { - if (!clientSite.endpoint) { - continue; - } - - // Add this site as a destination for the client - if (!mappings[clientSite.endpoint]) { - mappings[clientSite.endpoint] = { destinations: [] }; - } - - // Add site as a destination for this client - const destination: PeerDestination = { - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort - }; - - // Check if this destination is already in the array to avoid duplicates - const isDuplicate = mappings[clientSite.endpoint].destinations.some( - (dest) => - dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[clientSite.endpoint].destinations.push(destination); - } - } - - // Also handle site-to-site communication (all sites in the same org) - if (site.orgId) { - const orgSites = await db - .select() - .from(sites) - .where(eq(sites.orgId, site.orgId)); - - for (const peer of orgSites) { - // Skip self - if ( - peer.siteId === site.siteId || - !peer.endpoint || - !peer.subnet || - !peer.listenPort - ) { - continue; - } - - // Add peer site as a destination for this site - if (!mappings[site.endpoint]) { - mappings[site.endpoint] = { destinations: [] }; - } - - const destination: PeerDestination = { - destinationIP: peer.subnet.split("/")[0], - destinationPort: peer.listenPort - }; - - // Check for duplicates - const isDuplicate = mappings[site.endpoint].destinations.some( - (dest) => - dest.destinationIP === destination.destinationIP && - dest.destinationPort === destination.destinationPort - ); - - if (!isDuplicate) { - mappings[site.endpoint].destinations.push(destination); - } - } - } - } - - return mappings; -} diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index 9c6f2652..ee742c21 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -1,20 +1,21 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -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 { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { sites, resources, targets, exitNodes } from '@server/db/schemas'; +import { db } from '@server/db'; +import { eq } from 'drizzle-orm'; +import response from "@server/lib/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import logger from '@server/logger'; import config from "@server/lib/config"; -import { fromError } from "zod-validation-error"; -import { getAllowedIps } from "../target/helpers"; -import { createExitNode } from "@server/routers/gerbil/createExitNode"; - +import { getUniqueExitNodeEndpointName } from '@server/db/names'; +import { findNextAvailableCidr } from "@server/lib/ip"; +import { fromError } from 'zod-validation-error'; +import { getAllowedIps } from '../target/helpers'; // Define Zod schema for request validation const getConfigSchema = z.object({ publicKey: z.string(), - reachableAt: z.string().optional() + reachableAt: z.string().optional(), }); export type GetConfigResponse = { @@ -24,13 +25,9 @@ export type GetConfigResponse = { publicKey: string | null; allowedIps: string[]; }[]; -}; +} -export async function getConfig( - req: Request, - res: Response, - next: NextFunction -): Promise { +export async function getConfig(req: Request, res: Response, next: NextFunction): Promise { try { // Validate request parameters const parsedParams = getConfigSchema.safeParse(req.body); @@ -46,75 +43,102 @@ export async function getConfig( const { publicKey, reachableAt } = parsedParams.data; if (!publicKey) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "publicKey is required") - ); + return next(createHttpError(HttpCode.BAD_REQUEST, 'publicKey is required')); } - const exitNode = await createExitNode(publicKey, reachableAt); + // Fetch exit node + let exitNodeQuery = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey)); + let exitNode; + if (exitNodeQuery.length === 0) { + const address = await getNextAvailableSubnet(); + // TODO: eventually we will want to get the next available port so that we can multiple exit nodes + // const listenPort = await getNextAvailablePort(); + const listenPort = config.getRawConfig().gerbil.start_port; + let subEndpoint = ""; + if (config.getRawConfig().gerbil.use_subdomain) { + subEndpoint = await getUniqueExitNodeEndpointName(); + } + + // create a new exit node + exitNode = await db.insert(exitNodes).values({ + publicKey, + endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`, + address, + listenPort, + reachableAt, + name: `Exit Node ${publicKey.slice(0, 8)}`, + }).returning().execute(); + + logger.info(`Created new exit node ${exitNode[0].name} with address ${exitNode[0].address} and port ${exitNode[0].listenPort}`); + } else { + exitNode = exitNodeQuery; + } if (!exitNode) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create exit node" - ) - ); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to create exit node")); } - const configResponse = await generateGerbilConfig(exitNode); + // Fetch sites for this exit node + const sitesRes = await db.query.sites.findMany({ + where: eq(sites.exitNodeId, exitNode[0].exitNodeId), + }); + + const peers = await Promise.all(sitesRes.map(async (site) => { + return { + publicKey: site.pubKey, + allowedIps: await getAllowedIps(site.siteId) + }; + })); + + const configResponse: GetConfigResponse = { + listenPort: exitNode[0].listenPort || 51820, + ipAddress: exitNode[0].address, + peers, + }; logger.debug("Sending config: ", configResponse); return res.status(HttpCode.OK).send(configResponse); } catch (error) { logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) - ); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); } } -export async function generateGerbilConfig(exitNode: ExitNode) { - const sitesRes = await db - .select() - .from(sites) - .where( - and( - eq(sites.exitNodeId, exitNode.exitNodeId), - isNotNull(sites.pubKey), - isNotNull(sites.subnet) - ) - ); +async function getNextAvailableSubnet(): Promise { + // Get all existing subnets from routes table + const existingAddresses = await db.select({ + address: exitNodes.address, + }).from(exitNodes); - const peers = await Promise.all( - sitesRes.map(async (site) => { - if (site.type === "wireguard") { - return { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }; - } else if (site.type === "newt") { - return { - publicKey: site.pubKey, - allowedIps: [site.subnet!] - }; - } - return { - publicKey: null, - allowedIps: [] - }; - }) - ); + const addresses = existingAddresses.map(a => a.address); + let subnet = findNextAvailableCidr(addresses, config.getRawConfig().gerbil.block_size, config.getRawConfig().gerbil.subnet_group); + if (!subnet) { + throw new Error('No available subnets remaining in space'); + } - const configResponse: GetConfigResponse = { - listenPort: exitNode.listenPort || 51820, - ipAddress: exitNode.address, - peers - }; - - return configResponse; + // replace the last octet with 1 + subnet = subnet.split('.').slice(0, 3).join('.') + '.1' + '/' + subnet.split('/')[1]; + return subnet; +} + +async function getNextAvailablePort(): Promise { + // Get all existing ports from exitNodes table + const existingPorts = await db.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 deleted file mode 100644 index cd0e0b55..00000000 --- a/server/routers/gerbil/getResolvedHostname.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; - -// Define Zod schema for request validation -const getResolvedHostnameSchema = z.object({ - hostname: z.string(), - publicKey: z.string() -}); - -export async function getResolvedHostname( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - let endpoints: string[] = []; // always route locally - - // return the endpoints - return res.status(HttpCode.OK).send({ - endpoints - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) - ); - } -} diff --git a/server/routers/gerbil/index.ts b/server/routers/gerbil/index.ts index bff57d05..82f82c4c 100644 --- a/server/routers/gerbil/index.ts +++ b/server/routers/gerbil/index.ts @@ -1,5 +1,2 @@ export * from "./getConfig"; export * from "./receiveBandwidth"; -export * from "./updateHolePunch"; -export * from "./getAllRelays"; -export * from "./getResolvedHostname"; \ No newline at end of file diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts index 1cdc9184..47527ea0 100644 --- a/server/routers/gerbil/peers.ts +++ b/server/routers/gerbil/peers.ts @@ -1,58 +1,55 @@ -import logger from "@server/logger"; -import { db } from "@server/db"; -import { exitNodes } from "@server/db"; -import { eq } from "drizzle-orm"; -import { sendToExitNode } from "@server/lib/exitNodes"; +import axios from 'axios'; +import logger from '@server/logger'; +import db from '@server/db'; +import { exitNodes } from '@server/db/schemas'; +import { eq } from 'drizzle-orm'; -export async function addPeer( - exitNodeId: number, - peer: { - publicKey: string; - allowedIps: string[]; - } -) { - logger.info( - `Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}` - ); - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeId)) - .limit(1); +export async function addPeer(exitNodeId: number, peer: { + publicKey: string; + allowedIps: string[]; +}) { + + const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } + if (!exitNode.reachableAt) { + throw new Error(`Exit node with ID ${exitNodeId} is not reachable`); + } - return await sendToExitNode(exitNode, { - remoteType: "remoteExitNode/peers/add", - localPath: "/peer", - method: "POST", - data: peer - }); + try { + const response = await axios.post(`${exitNode.reachableAt}/peer`, peer, { + headers: { + 'Content-Type': 'application/json', + } + }); + + logger.info('Peer added successfully:', response.data.status); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`HTTP error! status: ${error.response?.status}`); + } + throw error; + } } export async function deletePeer(exitNodeId: number, publicKey: string) { - logger.info( - `Deleting peer with public key ${publicKey} from exit node ${exitNodeId}` - ); - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeId)) - .limit(1); + const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1); if (!exitNode) { throw new Error(`Exit node with ID ${exitNodeId} not found`); } - - return await sendToExitNode(exitNode, { - remoteType: "remoteExitNode/peers/remove", - localPath: "/peer", - method: "DELETE", - data: { - publicKey: publicKey - }, - queryParams: { - public_key: publicKey + if (!exitNode.reachableAt) { + throw new Error(`Exit node with ID ${exitNodeId} is not reachable`); + } + try { + const response = await axios.delete(`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`); + logger.info('Peer deleted successfully:', response.data.status); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`HTTP error! status: ${error.response?.status}`); } - }); + throw error; + } } diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index ca878633..a6c1e791 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -1,15 +1,11 @@ import { Request, Response, NextFunction } from "express"; -import { eq, and, lt, inArray, sql } from "drizzle-orm"; -import { sites } from "@server/db"; -import { db } from "@server/db"; +import { DrizzleError, eq } from "drizzle-orm"; +import { sites, resources, targets, exitNodes } from "@server/db/schemas"; +import db from "@server/db"; import logger from "@server/logger"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; -import { checkExitNodeOrg } from "@server/lib/exitNodes"; - -// Track sites that are already offline to avoid unnecessary queries -const offlineSites = new Set(); interface PeerBandwidth { publicKey: string; @@ -29,13 +25,54 @@ export const receiveBandwidth = async ( throw new Error("Invalid bandwidth data"); } - await updateSiteBandwidth(bandwidthData); // we are checking the usage on saas only + await db.transaction(async (trx) => { + for (const peer of bandwidthData) { + const { publicKey, bytesIn, bytesOut } = peer; + + // Find the site by public key + const site = await trx.query.sites.findFirst({ + where: eq(sites.pubKey, publicKey) + }); + + if (!site) { + logger.warn(`Site not found for public key: ${publicKey}`); + continue; + } + let online = site.online; + + // if the bandwidth for the site is > 0 then set it to online. if it has been less than 0 (no update) for 5 minutes then set it to offline + if (bytesIn > 0 || bytesOut > 0) { + online = true; + } else if (site.lastBandwidthUpdate) { + const lastBandwidthUpdate = new Date( + site.lastBandwidthUpdate + ); + const currentTime = new Date(); + const diff = + currentTime.getTime() - lastBandwidthUpdate.getTime(); + if (diff < 300000) { + online = false; + } + } + + // Update the site's bandwidth usage + await trx + .update(sites) + .set({ + megabytesOut: (site.megabytesOut || 0) + bytesIn, + megabytesIn: (site.megabytesIn || 0) + bytesOut, + lastBandwidthUpdate: new Date().toISOString(), + online + }) + .where(eq(sites.siteId, site.siteId)); + } + }); return response(res, { data: {}, success: true, error: false, - message: "Bandwidth data updated successfully", + message: "Organization retrieved successfully", status: HttpCode.OK }); } catch (error) { @@ -49,145 +86,8 @@ export const receiveBandwidth = async ( } }; -export async function updateSiteBandwidth( - bandwidthData: PeerBandwidth[], - exitNodeId?: number -) { - const currentTime = new Date(); - const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago - - // logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`); - - await db.transaction(async (trx) => { - // First, handle sites that are actively reporting bandwidth - const activePeers = bandwidthData.filter((peer) => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages - - if (activePeers.length > 0) { - // Remove any active peers from offline tracking since they're sending data - activePeers.forEach((peer) => offlineSites.delete(peer.publicKey)); - - // Aggregate usage data by organization - const orgUsageMap = new Map(); - const orgUptimeMap = new Map(); - - // Update all active sites with bandwidth data and get the site data in one operation - const updatedSites = []; - for (const peer of activePeers) { - const [updatedSite] = await trx - .update(sites) - .set({ - megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`, - megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`, - lastBandwidthUpdate: currentTime.toISOString(), - online: true - }) - .where(eq(sites.pubKey, peer.publicKey)) - .returning({ - online: sites.online, - orgId: sites.orgId, - siteId: sites.siteId, - lastBandwidthUpdate: sites.lastBandwidthUpdate - }); - - if (updatedSite) { - 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 }); - } - } - - // Calculate org usage aggregations using the updated site data - for (const { peer, ...site } of updatedSites) { - // Aggregate bandwidth usage for the org - const totalBandwidth = peer.bytesIn + peer.bytesOut; - const currentOrgUsage = orgUsageMap.get(site.orgId) || 0; - orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth); - - // Add 10 seconds of uptime for each active site - const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0; - orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds - } - } - - // Handle sites that reported zero bandwidth but need online status updated - const zeroBandwidthPeers = bandwidthData.filter( - (peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages - ); - - if (zeroBandwidthPeers.length > 0) { - const zeroBandwidthSites = await trx - .select() - .from(sites) - .where( - inArray( - sites.pubKey, - zeroBandwidthPeers.map((p) => p.publicKey) - ) - ); - - for (const site of zeroBandwidthSites) { - let newOnlineStatus = site.online; - - // Check if site should go offline based on last bandwidth update WITH DATA - if (site.lastBandwidthUpdate) { - const lastUpdateWithData = new Date( - site.lastBandwidthUpdate - ); - if (lastUpdateWithData < oneMinuteAgo) { - newOnlineStatus = false; - } - } else { - // No previous data update recorded, set to offline - newOnlineStatus = false; - } - - // Always update lastBandwidthUpdate to show this instance is receiving reports - // Only update online status if it changed - if (site.online !== newOnlineStatus) { - const [updatedSite] = await trx - .update(sites) - .set({ - online: newOnlineStatus - }) - .where(eq(sites.siteId, site.siteId)) - .returning(); - - if (updatedSite && exitNodeId) { - if ( - await checkExitNodeOrg( - exitNodeId, - updatedSite.orgId - ) - ) { - // not allowed - logger.warn( - `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` - ); - // THIS SHOULD TRIGGER THE TRANSACTION TO FAIL? - throw new Error("Exit node not allowed"); - } - } - - // If site went offline, add it to our tracking set - if (!newOnlineStatus && site.pubKey) { - offlineSites.add(site.pubKey); - } - } - } - } - }); +function calculateSubnet(index: number): string { + const baseIp = 10 << 24; + const subnetSize = 16; + return `${(baseIp | (index * subnetSize)).toString()}/28`; } diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts deleted file mode 100644 index b5885314..00000000 --- a/server/routers/gerbil/updateHolePunch.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { - clients, - newts, - olms, - Site, - sites, - clientSites, - exitNodes, - ExitNode -} from "@server/db"; -import { db } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -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 { checkExitNodeOrg } from "@server/lib/exitNodes"; - -// Define Zod schema for request validation -const updateHolePunchSchema = z.object({ - olmId: z.string().optional(), - newtId: z.string().optional(), - token: z.string(), - ip: z.string(), - port: z.number(), - timestamp: z.number(), - reachableAt: z.string().optional(), - publicKey: z.string().optional() -}); - -// New response type with multi-peer destination support -interface PeerDestination { - destinationIP: string; - destinationPort: number; -} - -export async function updateHolePunch( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - // Validate request parameters - const parsedParams = updateHolePunchSchema.safeParse(req.body); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { - olmId, - newtId, - ip, - port, - timestamp, - token, - reachableAt, - publicKey - } = parsedParams.data; - - let exitNode: ExitNode | undefined; - if (publicKey) { - // Get the exit node by public key - [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.publicKey, publicKey)); - } else { - // FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0 - [exitNode] = await db.select().from(exitNodes).limit(1); - } - - if (!exitNode) { - logger.warn(`Exit node not found for publicKey: ${publicKey}`); - return next( - createHttpError(HttpCode.NOT_FOUND, "Exit node not found") - ); - } - - const destinations = await updateAndGenerateEndpointDestinations( - olmId, - newtId, - ip, - port, - timestamp, - token, - exitNode - ); - - logger.debug( - `Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}` - ); - - // Return the new multi-peer structure - return res.status(HttpCode.OK).send({ - destinations: destinations - }); - } catch (error) { - // logger.error(error); // FIX THIS - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) - ); - } -} - -export async function updateAndGenerateEndpointDestinations( - olmId: string | undefined, - newtId: string | undefined, - ip: string, - port: number, - timestamp: number, - token: string, - exitNode: ExitNode, - checkOrg = false -) { - let currentSiteId: number | undefined; - const destinations: PeerDestination[] = []; - - if (olmId) { - logger.debug( - `Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}` - ); - - const { session, olm: olmSession } = - await validateOlmSessionToken(token); - if (!session || !olmSession) { - throw new Error("Unauthorized"); - } - - if (olmId !== olmSession.olmId) { - logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`); - throw new Error("Unauthorized"); - } - - const [olm] = await db.select().from(olms).where(eq(olms.olmId, olmId)); - - if (!olm || !olm.clientId) { - logger.warn(`Olm not found: ${olmId}`); - throw new Error("Olm not found"); - } - - const [client] = await db - .update(clients) - .set({ - lastHolePunch: timestamp - }) - .where(eq(clients.clientId, olm.clientId)) - .returning(); - - if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId) && checkOrg) { - // not allowed - logger.warn( - `Exit node ${exitNode.exitNodeId} is not allowed for org ${client.orgId}` - ); - throw new Error("Exit node not allowed"); - } - - // Get sites that are on this specific exit node and connected to this client - const sitesOnExitNode = await db - .select({ - siteId: sites.siteId, - subnet: sites.subnet, - listenPort: sites.listenPort - }) - .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .where( - and( - eq(sites.exitNodeId, exitNode.exitNodeId), - eq(clientSites.clientId, olm.clientId) - ) - ); - - // Update clientSites for each site on this exit node - for (const site of sitesOnExitNode) { - logger.debug( - `Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}` - ); - - await db - .update(clientSites) - .set({ - endpoint: `${ip}:${port}` - }) - .where( - and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, site.siteId) - ) - ); - } - - logger.debug( - `Updated ${sitesOnExitNode.length} sites on exit node ${exitNode.exitNodeId}` - ); - if (!client) { - logger.warn(`Client not found for olm: ${olmId}`); - throw new Error("Client not found"); - } - - // Create a list of the destinations from the sites - for (const site of sitesOnExitNode) { - if (site.subnet && site.listenPort) { - destinations.push({ - destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort - }); - } - } - } else if (newtId) { - logger.debug( - `Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}` - ); - - const { session, newt: newtSession } = - await validateNewtSessionToken(token); - - if (!session || !newtSession) { - throw new Error("Unauthorized"); - } - - if (newtId !== newtSession.newtId) { - logger.warn( - `Newt ID mismatch: ${newtId} !== ${newtSession.newtId}` - ); - throw new Error("Unauthorized"); - } - - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.newtId, newtId)); - - if (!newt || !newt.siteId) { - logger.warn(`Newt not found: ${newtId}`); - throw new Error("Newt not found"); - } - - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, newt.siteId)) - .limit(1); - - if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId) && checkOrg) { - // not allowed - logger.warn( - `Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}` - ); - throw new Error("Exit node not allowed"); - } - - currentSiteId = newt.siteId; - - // Update the current site with the new endpoint - const [updatedSite] = await db - .update(sites) - .set({ - endpoint: `${ip}:${port}`, - lastHolePunch: timestamp - }) - .where(eq(sites.siteId, newt.siteId)) - .returning(); - - if (!updatedSite || !updatedSite.subnet) { - logger.warn(`Site not found: ${newt.siteId}`); - throw new Error("Site not found"); - } - - // Find all clients that connect to this site - // const sitesClientPairs = await db - // .select() - // .from(clientSites) - // .where(eq(clientSites.siteId, newt.siteId)); - - // THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING - // Get client details for each client - // for (const pair of sitesClientPairs) { - // const [client] = await db - // .select() - // .from(clients) - // .where(eq(clients.clientId, pair.clientId)); - - // if (client && client.endpoint) { - // const [host, portStr] = client.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: parseInt(portStr, 10) - // }); - // } - // } - // } - - // If this is a newt/site, also add other sites in the same org - // if (updatedSite.orgId) { - // const orgSites = await db - // .select() - // .from(sites) - // .where(eq(sites.orgId, updatedSite.orgId)); - - // for (const site of orgSites) { - // // Don't add the current site to the destinations - // if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) { - // const [host, portStr] = site.endpoint.split(':'); - // if (host && portStr) { - // destinations.push({ - // destinationIP: host, - // destinationPort: site.listenPort - // }); - // } - // } - // } - // } - } - return destinations; -} diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index 63ce2edb..4f976b4c 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -8,7 +8,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { eq, and } from "drizzle-orm"; -import { idp, idpOrg } from "@server/db"; +import { idp, idpOrg } from "@server/db/schemas"; const paramsSchema = z .object({ diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 67357d76..e7fc6a5b 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -7,7 +7,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db"; +import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; @@ -67,7 +67,7 @@ export async function createOidcIdp( ); } - const { + let { clientId, clientSecret, authUrl, @@ -80,7 +80,7 @@ export async function createOidcIdp( autoProvision } = parsedBody.data; - const key = config.getRawConfig().server.secret!; + const key = config.getRawConfig().server.secret; const encryptedSecret = encrypt(clientSecret, key); const encryptedClientId = encrypt(clientId, key); @@ -111,7 +111,7 @@ export async function createOidcIdp( }); }); - const redirectUrl = await generateOidcRedirectUrl(idpId as number); + const redirectUrl = generateOidcRedirectUrl(idpId as number); return response(res, { data: { diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index 58b231b7..ac84c4f7 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -6,13 +6,12 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig, idpOrg } from "@server/db"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; import { eq } from "drizzle-orm"; 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/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index cd71929f..51b82554 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOrg } from "@server/db"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 242dfa37..371a2c21 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -6,10 +6,11 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig, idpOrg } from "@server/db"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; 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"; @@ -26,10 +27,6 @@ const bodySchema = z }) .strict(); -const querySchema = z.object({ - orgId: z.string().optional() // check what actuall calls it -}); - const ensureTrailingSlash = (url: string): string => { return url; }; @@ -68,18 +65,6 @@ 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) @@ -95,23 +80,6 @@ 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) => { @@ -121,7 +89,7 @@ export async function generateOidcUrl( return scope.length > 0; }); - const key = config.getRawConfig().server.secret!; + const key = config.getRawConfig().server.secret; const decryptedClientId = decrypt( existingIdp.idpOidcConfig.clientId, @@ -132,12 +100,7 @@ export async function generateOidcUrl( key ); - const redirectUrl = await generateOidcRedirectUrl(idpId, orgId); - logger.debug("OIDC client info", { - decryptedClientId, - decryptedClientSecret, - redirectUrl - }); + const redirectUrl = generateOidcRedirectUrl(idpId); const client = new arctic.OAuth2Client( decryptedClientId, decryptedClientSecret, @@ -153,6 +116,7 @@ export async function generateOidcUrl( codeVerifier, parsedScopes ); + logger.debug("Generated OIDC URL", { url }); const stateJwt = jsonwebtoken.sign( { @@ -160,7 +124,7 @@ export async function generateOidcUrl( state, codeVerifier }, - config.getRawConfig().server.secret! + config.getRawConfig().server.secret ); res.cookie("p_oidc_state", stateJwt, { diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index a202f4ea..794daade 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idp, idpOidcConfig } from "@server/db"; +import { idp, idpOidcConfig } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -65,7 +65,7 @@ export async function getIdp( return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found")); } - const key = config.getRawConfig().server.secret!; + const key = config.getRawConfig().server.secret; if (idpRes.idp.type === "oidc") { const clientSecret = idpRes.idpOidcConfig!.clientSecret; diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index f0dcf02e..37491388 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 "./listIdpOrgPolicies"; export * from "./updateIdpOrgPolicy"; +export * from "./listIdpOrgPolicies"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index 150e3f1d..b2105f45 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idpOrg, type IdpOrg } from "@server/db"; +import { idpOrg, type IdpOrg } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 150b9f88..a723ee05 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, idpOidcConfig } from "@server/db"; -import { domains, idp, orgDomains, users, idpOrg } from "@server/db"; +import { db } from "@server/db"; +import { domains, idp, orgDomains, users, idpOrg } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { eq, sql } from "drizzle-orm"; +import { sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -33,21 +33,23 @@ async function query(limit: number, offset: number) { idpId: idp.idpId, name: idp.name, type: idp.type, - variant: idpOidcConfig.variant, - orgCount: sql`count(${idpOrg.orgId})`, - autoProvision: idp.autoProvision + orgCount: sql`count(${idpOrg.orgId})` }) .from(idp) .leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`) - .leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId)) - .groupBy(idp.idpId, idpOidcConfig.variant) + .groupBy(idp.idpId) .limit(limit) .offset(offset); return res; } export type ListIdpsResponse = { - idps: Awaited>; + idps: Array<{ + idpId: number; + name: string; + type: string; + orgCount: number; + }>; pagination: { total: number; limit: number; diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts index 8bde3c0c..26873690 100644 --- a/server/routers/idp/oidcAutoProvision.ts +++ b/server/routers/idp/oidcAutoProvision.ts @@ -7,8 +7,8 @@ import { serializeSessionCookie } from "@server/auth/sessions/app"; import logger from "@server/logger"; +import db from "@server/db"; import { - db, Idp, idpOrg, orgs, @@ -16,12 +16,10 @@ import { User, userOrgs, users -} from "@server/db"; +} from "@server/db/schemas"; 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(); @@ -125,22 +123,6 @@ 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) => { @@ -176,13 +158,8 @@ export async function oidcAutoProvision({ .from(userOrgs) .where(eq(userOrgs.userId, userId)); - // Filter to only auto-provisioned orgs for CRUD operations - const autoProvisionedOrgs = currentUserOrgs.filter( - (org) => org.autoProvisioned === true - ); - // Delete orgs that are no longer valid - const orgsToDelete = autoProvisionedOrgs + const orgsToDelete = currentUserOrgs .filter( (currentOrg) => !userOrgInfo.some( @@ -218,9 +195,7 @@ export async function oidcAutoProvision({ orgsToAdd.map((org) => ({ userId: userId!, orgId: org.orgId, - roleId: org.roleId, - autoProvisioned: true, - dateCreated: new Date().toISOString() + roleId: org.roleId })) ); } diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index d5a00de7..642837da 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -7,8 +7,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { idp, idpOrg } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; -import { idp, idpOrg } from "@server/db"; const paramsSchema = z .object({ diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 53ece68e..49a16a52 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -7,7 +7,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { idp, idpOidcConfig } from "@server/db"; +import { idp, idpOidcConfig } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; @@ -84,7 +84,7 @@ export async function updateOidcIdp( } const { idpId } = parsedParams.data; - const { + let { clientId, clientSecret, authUrl, @@ -118,7 +118,7 @@ export async function updateOidcIdp( ); } - const key = config.getRawConfig().server.secret!; + const key = config.getRawConfig().server.secret; const encryptedSecret = clientSecret ? encrypt(clientSecret, key) : undefined; diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index dd7331bd..274350d9 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,17 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Org } from "@server/db"; +import { db } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; -import createHttpError, { HttpError } from "http-errors"; +import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { - idp, - idpOidcConfig, - users -} from "@server/db"; -import { and, eq } from "drizzle-orm"; +import { idp, idpOidcConfig, users } from "@server/db/schemas"; +import { and, eq, inArray } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import jmespath from "jmespath"; @@ -41,10 +37,6 @@ const bodySchema = z.object({ storedState: z.string().nonempty() }); -const querySchema = z.object({ - loginPageId: z.coerce.number().optional() -}); - export type ValidateOidcUrlCallbackResponse = { redirectUrl: string; }; @@ -77,18 +69,6 @@ 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 @@ -106,7 +86,7 @@ export async function validateOidcCallback( ); } - const key = config.getRawConfig().server.secret!; + const key = config.getRawConfig().server.secret; const decryptedClientId = decrypt( existingIdp.idpOidcConfig.clientId, @@ -117,11 +97,7 @@ export async function validateOidcCallback( key ); - const redirectUrl = await generateOidcRedirectUrl( - existingIdp.idp.idpId, - undefined, - loginPageId - ); + const redirectUrl = generateOidcRedirectUrl(existingIdp.idp.idpId); const client = new arctic.OAuth2Client( decryptedClientId, decryptedClientSecret, @@ -130,7 +106,7 @@ export async function validateOidcCallback( const statePayload = jsonwebtoken.verify( storedState, - config.getRawConfig().server.secret!, + config.getRawConfig().server.secret, function (err, decoded) { if (err) { logger.error("Error verifying state JWT", { err }); @@ -176,12 +152,6 @@ export async function validateOidcCallback( ); } - logger.debug("State verified", { - urL: ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), - expectedState, - state - }); - const tokens = await client.validateAuthorizationCode( ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), code, @@ -189,14 +159,12 @@ export async function validateOidcCallback( ); const idToken = tokens.idToken(); - logger.debug("ID token", { idToken }); const claims = arctic.decodeIdToken(idToken); - logger.debug("ID token claims", { claims }); - let userIdentifier = jmespath.search( + const userIdentifier = jmespath.search( claims, existingIdp.idpOidcConfig.identifierPath - ) as string | null; + ); if (!userIdentifier) { return next( @@ -207,8 +175,6 @@ export async function validateOidcCallback( ); } - userIdentifier = userIdentifier.toLowerCase(); - logger.debug("User identifier", { userIdentifier }); let email = null; @@ -232,10 +198,6 @@ export async function validateOidcCallback( logger.debug("User email", { email }); logger.debug("User name", { name }); - if (email) { - email = email.toLowerCase(); - } - const [existingUser] = await db .select() .from(users) @@ -247,21 +209,16 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - 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; - } + await oidcAutoProvision({ + idp: existingIdp.idp, + userIdentifier, + email, + name, + claims, + existingUser, + req, + res + }); return response(res, { data: { @@ -310,13 +267,3 @@ export async function validateOidcCallback( ); } } - -function hydrateOrgMapping( - orgMapping: string | null, - orgId: string -): string | undefined { - if (!orgMapping) { - return undefined; - } - return orgMapping.split("{{orgId}}").join(orgId); -} diff --git a/server/routers/integration.ts b/server/routers/integration.ts deleted file mode 100644 index 3513cc64..00000000 --- a/server/routers/integration.ts +++ /dev/null @@ -1,652 +0,0 @@ -import * as site from "./site"; -import * as org from "./org"; -import * as resource from "./resource"; -import * as domain from "./domain"; -import * as target from "./target"; -import * as user from "./user"; -import * as role from "./role"; -import * as client from "./client"; -import * as accessToken from "./accessToken"; -import * as apiKeys from "./apiKeys"; -import * as idp from "./idp"; -import * as siteResource from "./siteResource"; -import { - verifyApiKey, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction, - verifyApiKeySiteAccess, - verifyApiKeyResourceAccess, - verifyApiKeyTargetAccess, - verifyApiKeyRoleAccess, - verifyApiKeyUserAccess, - verifyApiKeySetResourceUsers, - verifyApiKeyAccessTokenAccess, - verifyApiKeyIsRoot, - verifyApiKeyClientAccess, - verifyClientsEnabled, - verifyApiKeySiteResourceAccess -} from "@server/middlewares"; -import HttpCode from "@server/types/HttpCode"; -import { Router } from "express"; -import { ActionsEnum } from "@server/auth/actions"; - -export const unauthenticated = Router(); - -unauthenticated.get("/", (_, res) => { - res.status(HttpCode.OK).json({ message: "Healthy" }); -}); - -export const authenticated = Router(); -authenticated.use(verifyApiKey); - -authenticated.get( - "/org/checkId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.checkOrgId), - org.checkId -); - -authenticated.put( - "/org", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createOrg), - org.createOrg -); - -authenticated.get( - "/orgs", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listOrgs), - org.listOrgs -); // TODO we need to check the orgs here - -authenticated.get( - "/org/:orgId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getOrg), - org.getOrg -); - -authenticated.post( - "/org/:orgId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.updateOrg), - org.updateOrg -); - -authenticated.delete( - "/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteOrg), - org.deleteOrg -); - -authenticated.put( - "/org/:orgId/site", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createSite), - site.createSite -); - -authenticated.get( - "/org/:orgId/sites", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listSites), - site.listSites -); - -authenticated.get( - "/org/:orgId/site/:niceId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getSite), - site.getSite -); - -authenticated.get( - "/org/:orgId/pick-site-defaults", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createSite), - site.pickSiteDefaults -); - -authenticated.get( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.getSite), - site.getSite -); - -authenticated.post( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.updateSite), - site.updateSite -); - -authenticated.delete( - "/site/:siteId", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.deleteSite), - site.deleteSite -); - -authenticated.get( - "/org/:orgId/user-resources", - verifyApiKeyOrgAccess, - resource.getUserResources -); -// Site Resource endpoints -authenticated.put( - "/org/:orgId/site/:siteId/resource", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.createSiteResource), - siteResource.createSiteResource -); - -authenticated.get( - "/org/:orgId/site/:siteId/resources", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.listSiteResources), - siteResource.listSiteResources -); - -authenticated.get( - "/org/:orgId/site-resources", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listSiteResources), - siteResource.listAllSiteResourcesByOrg -); - -authenticated.get( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, - verifyApiKeySiteResourceAccess, - verifyApiKeyHasAction(ActionsEnum.getSiteResource), - siteResource.getSiteResource -); - -authenticated.post( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, - verifyApiKeySiteResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateSiteResource), - siteResource.updateSiteResource -); - -authenticated.delete( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, - verifyApiKeySiteResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteSiteResource), - siteResource.deleteSiteResource -); - -authenticated.put( - "/org/:orgId/resource", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createResource), - resource.createResource -); - -authenticated.put( - "/org/:orgId/site/:siteId/resource", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createResource), - resource.createResource -); - -authenticated.get( - "/site/:siteId/resources", - verifyApiKeySiteAccess, - verifyApiKeyHasAction(ActionsEnum.listResources), - resource.listResources -); - -authenticated.get( - "/org/:orgId/resources", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listResources), - resource.listResources -); - -authenticated.get( - "/org/:orgId/domains", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listOrgDomains), - domain.listDomains -); - -authenticated.get( - "/org/:orgId/invitations", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listInvitations), - user.listInvitations -); - -authenticated.post( - "/org/:orgId/create-invite", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.inviteUser), - user.inviteUser -); - -authenticated.get( - "/resource/:resourceId/roles", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceRoles), - resource.listResourceRoles -); - -authenticated.get( - "/resource/:resourceId/users", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceUsers), - resource.listResourceUsers -); - -authenticated.get( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.getResource), - resource.getResource -); - -authenticated.post( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResource), - resource.updateResource -); - -authenticated.delete( - "/resource/:resourceId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteResource), - resource.deleteResource -); - -authenticated.put( - "/resource/:resourceId/target", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.createTarget), - target.createTarget -); - -authenticated.get( - "/resource/:resourceId/targets", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listTargets), - target.listTargets -); - -authenticated.put( - "/resource/:resourceId/rule", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.createResourceRule), - resource.createResourceRule -); - -authenticated.get( - "/resource/:resourceId/rules", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceRules), - resource.listResourceRules -); - -authenticated.post( - "/resource/:resourceId/rule/:ruleId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResourceRule), - resource.updateResourceRule -); - -authenticated.delete( - "/resource/:resourceId/rule/:ruleId", - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteResourceRule), - resource.deleteResourceRule -); - -authenticated.get( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.getTarget), - target.getTarget -); - -authenticated.post( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.updateTarget), - target.updateTarget -); - -authenticated.delete( - "/target/:targetId", - verifyApiKeyTargetAccess, - verifyApiKeyHasAction(ActionsEnum.deleteTarget), - target.deleteTarget -); - -authenticated.put( - "/org/:orgId/role", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createRole), - role.createRole -); - -authenticated.get( - "/org/:orgId/roles", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listRoles), - role.listRoles -); - -authenticated.delete( - "/role/:roleId", - verifyApiKeyRoleAccess, - verifyApiKeyHasAction(ActionsEnum.deleteRole), - role.deleteRole -); - -authenticated.get( - "/role/:roleId", - verifyApiKeyRoleAccess, - verifyApiKeyHasAction(ActionsEnum.getRole), - role.getRole -); - -authenticated.post( - "/role/:roleId/add/:userId", - verifyApiKeyRoleAccess, - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.addUserRole), - user.addUserRole -); - -authenticated.post( - "/resource/:resourceId/roles", - verifyApiKeyResourceAccess, - verifyApiKeyRoleAccess, - verifyApiKeyHasAction(ActionsEnum.setResourceRoles), - resource.setResourceRoles -); - -authenticated.post( - "/resource/:resourceId/users", - verifyApiKeyResourceAccess, - verifyApiKeySetResourceUsers, - verifyApiKeyHasAction(ActionsEnum.setResourceUsers), - resource.setResourceUsers -); - -authenticated.post( - `/resource/:resourceId/password`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourcePassword), - resource.setResourcePassword -); - -authenticated.post( - `/resource/:resourceId/pincode`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourcePincode), - resource.setResourcePincode -); - -authenticated.post( - `/resource/:resourceId/header-auth`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth), - resource.setResourceHeaderAuth -); - -authenticated.post( - `/resource/:resourceId/whitelist`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), - resource.setResourceWhitelist -); - -authenticated.get( - `/resource/:resourceId/whitelist`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist), - resource.getResourceWhitelist -); - -authenticated.post( - `/resource/:resourceId/access-token`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.generateAccessToken), - accessToken.generateAccessToken -); - -authenticated.delete( - `/access-token/:accessTokenId`, - verifyApiKeyAccessTokenAccess, - verifyApiKeyHasAction(ActionsEnum.deleteAcessToken), - accessToken.deleteAccessToken -); - -authenticated.get( - `/org/:orgId/access-tokens`, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listAccessTokens), - accessToken.listAccessTokens -); - -authenticated.get( - `/resource/:resourceId/access-tokens`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listAccessTokens), - accessToken.listAccessTokens -); - -authenticated.get( - "/org/:orgId/user/:userId", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.getOrgUser), - user.getOrgUser -); - -authenticated.post( - "/user/:userId/2fa", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.updateUser), - user.updateUser2FA -); - -authenticated.get( - "/user/:userId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.getUser), - user.adminGetUser -); - -authenticated.get( - "/org/:orgId/users", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listUsers), - user.listUsers -); - -authenticated.put( - "/org/:orgId/user", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createOrgUser), - user.createOrgUser -); - -authenticated.post( - "/org/:orgId/user/:userId", - verifyApiKeyOrgAccess, - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.updateOrgUser), - user.updateOrgUser -); - -authenticated.delete( - "/org/:orgId/user/:userId", - verifyApiKeyOrgAccess, - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.removeUser), - user.removeUserOrg -); - -// authenticated.put( -// "/newt", -// verifyApiKeyHasAction(ActionsEnum.createNewt), -// newt.createNewt -// ); - -authenticated.get( - `/org/:orgId/api-keys`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listApiKeys), - apiKeys.listOrgApiKeys -); - -authenticated.post( - `/org/:orgId/api-key/:apiKeyId/actions`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.setApiKeyActions), - apiKeys.setApiKeyActions -); - -authenticated.get( - `/org/:orgId/api-key/:apiKeyId/actions`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listApiKeyActions), - apiKeys.listApiKeyActions -); - -authenticated.put( - `/org/:orgId/api-key`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createApiKey), - apiKeys.createOrgApiKey -); - -authenticated.delete( - `/org/:orgId/api-key/:apiKeyId`, - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteApiKey), - apiKeys.deleteApiKey -); - -authenticated.put( - "/idp/oidc", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createIdp), - idp.createOidcIdp -); - -authenticated.post( - "/idp/:idpId/oidc", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.updateIdp), - idp.updateOidcIdp -); - -authenticated.get( - "/idp", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listIdps), - idp.listIdps -); - -authenticated.get( - "/idp/:idpId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.getIdp), - idp.getIdp -); - -authenticated.put( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.createIdpOrg), - idp.createIdpOrgPolicy -); - -authenticated.post( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.updateIdpOrg), - idp.updateIdpOrgPolicy -); - -authenticated.delete( - "/idp/:idpId/org/:orgId", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg), - idp.deleteIdpOrgPolicy -); - -authenticated.get( - "/idp/:idpId/org", - verifyApiKeyIsRoot, - verifyApiKeyHasAction(ActionsEnum.listIdpOrgs), - idp.listIdpOrgPolicies -); - -authenticated.get( - "/org/:orgId/pick-client-defaults", - verifyClientsEnabled, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createClient), - client.pickClientDefaults -); - -authenticated.get( - "/org/:orgId/clients", - verifyClientsEnabled, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.listClients), - client.listClients -); - -authenticated.get( - "/client/:clientId", - verifyClientsEnabled, - verifyApiKeyClientAccess, - verifyApiKeyHasAction(ActionsEnum.getClient), - client.getClient -); - -authenticated.put( - "/org/:orgId/client", - verifyClientsEnabled, - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.createClient), - client.createClient -); - -authenticated.delete( - "/client/:clientId", - verifyClientsEnabled, - verifyApiKeyClientAccess, - verifyApiKeyHasAction(ActionsEnum.deleteClient), - client.deleteClient -); - -authenticated.post( - "/client/:clientId", - verifyClientsEnabled, - verifyApiKeyClientAccess, - verifyApiKeyHasAction(ActionsEnum.updateClient), - client.updateClient -); - -authenticated.put( - "/org/:orgId/blueprint", - verifyApiKeyOrgAccess, - verifyApiKeyHasAction(ActionsEnum.applyBlueprint), - org.applyBlueprint -); diff --git a/server/routers/internal.ts b/server/routers/internal.ts index dc40ea27..fbc3f9ee 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -4,7 +4,6 @@ import * as traefik from "@server/routers/traefik"; 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 HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, @@ -12,7 +11,7 @@ import { } from "@server/middlewares"; // Root routes -export const internalRouter = Router(); +const internalRouter = Router(); internalRouter.get("/", (_, res) => { res.status(HttpCode.OK).json({ message: "Healthy" }); @@ -32,28 +31,18 @@ internalRouter.post( resource.getExchangeToken ); -internalRouter.get("/idp", idp.listIdps); - -internalRouter.get("/idp/:idpId", idp.getIdp); - // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); -// 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 gerbilRouter.post("/get-config", gerbil.getConfig); +gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); // Badger routes const badgerRouter = Router(); internalRouter.use("/badger", badgerRouter); badgerRouter.post("/verify-session", badger.verifyResourceSession); - badgerRouter.post("/exchange-session", badger.exchangeSession); + +export default internalRouter; diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts deleted file mode 100644 index 26f59cab..00000000 --- a/server/routers/loginPage/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/messageHandlers.ts b/server/routers/messageHandlers.ts new file mode 100644 index 00000000..9dd7756f --- /dev/null +++ b/server/routers/messageHandlers.ts @@ -0,0 +1,6 @@ +import { handleRegisterMessage } from "./newt"; +import { MessageHandler } from "./ws"; + +export const messageHandlers: Record = { + "newt/wg/register": handleRegisterMessage, +}; \ No newline at end of file diff --git a/server/routers/newt/createNewt.ts b/server/routers/newt/createNewt.ts index d54cd1a9..b69ada32 100644 --- a/server/routers/newt/createNewt.ts +++ b/server/routers/newt/createNewt.ts @@ -1,9 +1,9 @@ import { NextFunction, Request, Response } from "express"; -import { db } from "@server/db"; +import db from "@server/db"; import { hash } from "@node-rs/argon2"; import HttpCode from "@server/types/HttpCode"; import { z } from "zod"; -import { newts } from "@server/db"; +import { newts } from "@server/db/schemas"; import createHttpError from "http-errors"; import response from "@server/lib/response"; import { SqliteError } from "better-sqlite3"; diff --git a/server/routers/newt/dockerSocket.ts b/server/routers/newt/dockerSocket.ts deleted file mode 100644 index 071069fe..00000000 --- a/server/routers/newt/dockerSocket.ts +++ /dev/null @@ -1,22 +0,0 @@ -import NodeCache from "node-cache"; -import { sendToClient } from "@server/routers/ws"; - -export const dockerSocketCache = new NodeCache({ - stdTTL: 3600 // seconds -}); - -export function fetchContainers(newtId: string) { - const payload = { - type: `newt/socket/fetch`, - data: {} - }; - sendToClient(newtId, payload); -} - -export function dockerSocket(newtId: string) { - const payload = { - type: `newt/socket/check`, - data: {} - }; - sendToClient(newtId, payload); -} diff --git a/server/routers/newt/getNewtToken.ts b/server/routers/newt/getToken.ts similarity index 97% rename from server/routers/newt/getNewtToken.ts rename to server/routers/newt/getToken.ts index 3bf45dcf..7bf89ebf 100644 --- a/server/routers/newt/getNewtToken.ts +++ b/server/routers/newt/getToken.ts @@ -1,6 +1,6 @@ import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { newts } from "@server/db"; +import db from "@server/db"; +import { newts } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; @@ -24,7 +24,7 @@ export const newtGetTokenBodySchema = z.object({ export type NewtGetTokenBody = z.infer; -export async function getNewtToken( +export async function getToken( req: Request, res: Response, next: NextFunction diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts deleted file mode 100644 index 62802fff..00000000 --- a/server/routers/newt/handleApplyBlueprintMessage.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { db, newts } from "@server/db"; -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"; -import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint"; - -export const handleApplyBlueprintMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - logger.debug("Handling apply blueprint message!"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - if (!newt.siteId) { - logger.warn("Newt has no site!"); // TODO: Maybe we create the site here? - return; - } - - // get the site - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, newt.siteId)); - - if (!site) { - logger.warn("Site not found for newt"); - return; - } - - const { blueprint } = message.data; - if (!blueprint) { - logger.warn("No blueprint provided"); - return; - } - - logger.debug(`Received blueprint: ${blueprint}`); - - try { - const blueprintParsed = JSON.parse(blueprint); - // Update the blueprint in the database - await applyBlueprint(site.orgId, blueprintParsed, site.siteId); - } catch (error) { - logger.error(`Failed to update database from config: ${error}`); - return { - message: { - type: "newt/blueprint/results", - data: { - success: false, - message: `Failed to update database from config: ${error}` - } - }, - broadcast: false, // Send to all clients - excludeSender: false // Include sender in broadcast - }; - } - - return { - message: { - type: "newt/blueprint/results", - data: { - success: true, - message: "Config updated successfully" - } - }, - broadcast: false, // Send to all clients - excludeSender: false // Include sender in broadcast - }; -}; diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts deleted file mode 100644 index e08e4132..00000000 --- a/server/routers/newt/handleGetConfigMessage.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { z } from "zod"; -import { MessageHandler } from "@server/routers/ws"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { - db, - ExitNode, - exitNodes, - resources, - siteResources, - Target, - targets -} from "@server/db"; -import { clients, clientSites, Newt, sites } from "@server/db"; -import { eq, and, inArray } from "drizzle-orm"; -import { updatePeer } from "../olm/peers"; -import { sendToExitNode } from "@server/lib/exitNodes"; - -const inputSchema = z.object({ - publicKey: z.string(), - port: z.number().int().positive() -}); - -type Input = z.infer; - -export const handleGetConfigMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - const now = new Date().getTime() / 1000; - - logger.debug("Handling Newt get config message!"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - if (!newt.siteId) { - logger.warn("Newt has no site!"); // TODO: Maybe we create the site here? - return; - } - - const parsed = inputSchema.safeParse(message.data); - if (!parsed.success) { - logger.error( - "handleGetConfigMessage: Invalid input: " + - fromError(parsed.error).toString() - ); - return; - } - - const { publicKey, port } = message.data as Input; - const siteId = newt.siteId; - - // Get the current site data - const [existingSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)); - - if (!existingSite) { - logger.warn("handleGetConfigMessage: Site not found"); - return; - } - - // we need to wait for hole punch success - if (!existingSite.endpoint) { - logger.debug(`In newt get config: existing site ${existingSite.siteId} has no endpoint, skipping`); - return; - } - - if (existingSite.publicKey !== publicKey) { - // 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; - // } - - // update the endpoint and the public key - const [site] = await db - .update(sites) - .set({ - publicKey, - listenPort: port - }) - .where(eq(sites.siteId, siteId)) - .returning(); - - if (!site) { - logger.error("handleGetConfigMessage: Failed to update site"); - return; - } - - let exitNode: ExitNode | undefined; - if (site.exitNodeId) { - [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, site.exitNodeId)) - .limit(1); - if ( - exitNode.reachableAt && - existingSite.subnet && - existingSite.listenPort - ) { - const payload = { - oldDestination: { - destinationIP: existingSite.subnet?.split("/")[0], - destinationPort: existingSite.listenPort - }, - newDestination: { - destinationIP: site.subnet?.split("/")[0], - destinationPort: site.listenPort - } - }; - - await sendToExitNode(exitNode, { - remoteType: "remoteExitNode/update-proxy-mapping", - localPath: "/update-proxy-mapping", - method: "POST", - data: payload - }); - } - } - - // Get all clients connected to this site - const clientsRes = await db - .select() - .from(clients) - .innerJoin(clientSites, eq(clients.clientId, clientSites.clientId)) - .where(eq(clientSites.siteId, siteId)); - - // Prepare peers data for the response - const peers = await Promise.all( - clientsRes - .filter((client) => { - if (!client.clients.pubKey) { - return false; - } - if (!client.clients.subnet) { - return false; - } - return true; - }) - .map(async (client) => { - // Add or update this peer on the olm if it is connected - try { - if (!site.publicKey) { - logger.warn( - `Site ${site.siteId} has no public key, skipping` - ); - return null; - } - let endpoint = site.endpoint; - if (client.clientSites.isRelayed) { - if (!site.exitNodeId) { - logger.warn( - `Site ${site.siteId} has no exit node, skipping` - ); - return null; - } - - if (!exitNode) { - logger.warn( - `Exit node not found for site ${site.siteId}` - ); - return null; - } - endpoint = `${exitNode.endpoint}:21820`; - } - - if (!endpoint) { - logger.warn( - `In Newt get config: Peer site ${site.siteId} has no endpoint, skipping` - ); - return null; - } - - await updatePeer(client.clients.clientId, { - siteId: site.siteId, - endpoint: endpoint, - publicKey: site.publicKey, - serverIP: site.address, - serverPort: site.listenPort, - remoteSubnets: site.remoteSubnets - }); - } catch (error) { - logger.error( - `Failed to add/update peer ${client.clients.pubKey} to olm ${newt.newtId}: ${error}` - ); - } - - return { - publicKey: client.clients.pubKey!, - allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: client.clientSites.isRelayed - ? "" - : client.clientSites.endpoint! // if its relayed it should be localhost - }; - }) - ); - - // Filter out any null values from peers that didn't have an olm - const validPeers = peers.filter((peer) => peer !== null); - - // Get all enabled targets with their resource protocol information - const allSiteResources = await db - .select() - .from(siteResources) - .where(eq(siteResources.siteId, siteId)); - - const { tcpTargets, udpTargets } = allSiteResources.reduce( - (acc, resource) => { - // Filter out invalid targets - if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) { - return acc; - } - - // Format target into string - const formattedTarget = `${resource.proxyPort}:${resource.destinationIp}:${resource.destinationPort}`; - - // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(formattedTarget); - } else { - acc.udpTargets.push(formattedTarget); - } - - return acc; - }, - { tcpTargets: [] as string[], udpTargets: [] as string[] } - ); - - // Build the configuration response - const configResponse = { - ipAddress: site.address, - peers: validPeers, - targets: { - udp: udpTargets, - tcp: tcpTargets - } - }; - - logger.debug("Sending config: ", configResponse); - return { - message: { - type: "newt/wg/receive-config", - data: { - ...configResponse - } - }, - broadcast: false, - excludeSender: false - }; -}; diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts deleted file mode 100644 index 8b28c2a8..00000000 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { db, sites } from "@server/db"; -import { MessageHandler } from "@server/routers/ws"; -import { exitNodes, Newt } from "@server/db"; -import logger from "@server/logger"; -import { ne, eq, or, and, count } from "drizzle-orm"; -import { listExitNodes } from "@server/lib/exitNodes"; - -export const handleNewtPingRequestMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - logger.info("Handling ping request newt message!"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - // Get the newt's orgId through the site relationship - if (!newt.siteId) { - logger.warn("Newt siteId not found"); - return; - } - - const [site] = await db - .select({ orgId: sites.orgId }) - .from(sites) - .where(eq(sites.siteId, newt.siteId)) - .limit(1); - - 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) { - const [lastExitNode] = await db - .select() - .from(sites) - .where(eq(sites.siteId, newt.siteId)) - .limit(1); - lastExitNodeId = lastExitNode?.exitNodeId || null; - } - - const exitNodesPayload = await Promise.all( - exitNodesList.map(async (node) => { - // (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS) - // higher = more desirable - // like saying, this node has x% of its capacity left - - let weight = 1; - const maxConnections = node.maxConnections; - if (maxConnections !== null && maxConnections !== undefined) { - const [currentConnections] = await db - .select({ - count: count() - }) - .from(sites) - .where( - and( - eq(sites.exitNodeId, node.exitNodeId), - eq(sites.online, true) - ) - ); - - if (currentConnections.count >= maxConnections) { - return null; - } - - weight = - (maxConnections - currentConnections.count) / - maxConnections; - } - - return { - exitNodeId: node.exitNodeId, - exitNodeName: node.name, - endpoint: node.endpoint, - weight, - wasPreviouslyConnected: node.exitNodeId === lastExitNodeId - }; - }) - ); - - // filter out null values - const filteredExitNodes = exitNodesPayload.filter((node) => node !== null); - - return { - message: { - type: "newt/ping/exitNodes", - data: { - exitNodes: filteredExitNodes - } - }, - broadcast: false, // Send to all clients - excludeSender: false // Include sender in broadcast - }; -}; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts deleted file mode 100644 index 3df7822d..00000000 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ /dev/null @@ -1,340 +0,0 @@ -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"; -import config from "@server/lib/config"; -import { - findNextAvailableCidr, - getNextAvailableClientSubnet -} from "@server/lib/ip"; -import { - selectBestExitNode, - verifyExitNodeOrgAccess -} from "@server/lib/exitNodes"; -import { fetchContainers } from "./dockerSocket"; - -export type ExitNodePingResult = { - exitNodeId: number; - latencyMs: number; - weight: number; - error?: string; - exitNodeName: string; - endpoint: string; - wasPreviouslyConnected: boolean; -}; - -const numTimesLimitExceededForId: Record = {}; - -export const handleNewtRegisterMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - logger.debug("Handling register newt message!"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - if (!newt.siteId) { - logger.warn("Newt has no site!"); // TODO: Maybe we create the site here? - return; - } - - const siteId = newt.siteId; - - const { publicKey, pingResults, newtVersion, backwardsCompatible } = - message.data; - if (!publicKey) { - logger.warn("Public key not provided"); - return; - } - - if (backwardsCompatible) { - logger.debug( - "Backwards compatible mode detecting - not sending connect message and waiting for ping response." - ); - return; - } - - let exitNodeId: number | undefined; - if (pingResults) { - const bestPingResult = selectBestExitNode( - pingResults as ExitNodePingResult[] - ); - if (!bestPingResult) { - logger.warn("No suitable exit node found based on ping results"); - return; - } - exitNodeId = bestPingResult.exitNodeId; - } - - const [oldSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!oldSite) { - logger.warn("Site not found"); - return; - } - - logger.debug(`Docker socket enabled: ${oldSite.dockerSocketEnabled}`); - - if (oldSite.dockerSocketEnabled) { - logger.debug( - "Site has docker socket enabled - requesting docker containers" - ); - fetchContainers(newt.newtId); - } - - let siteSubnet = oldSite.subnet; - let exitNodeIdToQuery = oldSite.exitNodeId; - if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) { - // This effectively moves the exit node to the new one - exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId - - const { exitNode, hasAccess } = await verifyExitNodeOrgAccess( - exitNodeIdToQuery, - oldSite.orgId - ); - - if (!exitNode) { - logger.warn("Exit node not found"); - return; - } - - if (!hasAccess) { - logger.warn("Not authorized to use this exit node"); - return; - } - - const sitesQuery = await db - .select({ - subnet: sites.subnet - }) - .from(sites) - .where(eq(sites.exitNodeId, exitNodeId)); - - 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( - subnets, - blockSize, - exitNode.address - ); - if (!newSubnet) { - logger.error( - `No available subnets found for the new exit node id ${exitNodeId} and site id ${siteId}` - ); - return; - } - - siteSubnet = newSubnet; - - await db - .update(sites) - .set({ - pubKey: publicKey, - exitNodeId: exitNodeId, - subnet: newSubnet - }) - .where(eq(sites.siteId, siteId)) - .returning(); - } else { - await db - .update(sites) - .set({ - pubKey: publicKey - }) - .where(eq(sites.siteId, siteId)) - .returning(); - } - - if (!exitNodeIdToQuery) { - logger.warn("No exit node ID to query"); - return; - } - - const [exitNode] = await db - .select() - .from(exitNodes) - .where(eq(exitNodes.exitNodeId, exitNodeIdToQuery)) - .limit(1); - - if (oldSite.pubKey && oldSite.pubKey !== publicKey && oldSite.exitNodeId) { - logger.info("Public key mismatch. Deleting old peer..."); - await deletePeer(oldSite.exitNodeId, oldSite.pubKey); - } - - if (!siteSubnet) { - logger.warn("Site has no subnet"); - return; - } - - 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 - await db - .update(newts) - .set({ - version: newtVersion as string - }) - .where(eq(newts.newtId, newt.newtId)); - } - - // Get all enabled targets with their resource protocol information - const allTargets = await db - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled, - protocol: resources.protocol, - 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( - (acc, target) => { - // Filter out invalid targets - if (!target.internalPort || !target.ip || !target.port) { - return acc; - } - - // Format target into string - const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; - - // Add to the appropriate protocol array - if (target.protocol === "tcp") { - acc.tcpTargets.push(formattedTarget); - } else { - acc.udpTargets.push(formattedTarget); - } - - return acc; - }, - { 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", - data: { - endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`, - publicKey: exitNode.publicKey, - serverIP: exitNode.address.split("/")[0], - tunnelIP: siteSubnet.split("/")[0], - targets: { - udp: udpTargets, - tcp: tcpTargets - }, - healthCheckTargets: validHealthCheckTargets - } - }, - broadcast: false, // Send to all clients - excludeSender: false // Include sender in broadcast - }; -}; diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts deleted file mode 100644 index f5170feb..00000000 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { db } from "@server/db"; -import { MessageHandler } from "@server/routers/ws"; -import { clients, Newt } from "@server/db"; -import { eq } from "drizzle-orm"; -import logger from "@server/logger"; - -interface PeerBandwidth { - publicKey: string; - bytesIn: number; - bytesOut: number; -} - -export const handleReceiveBandwidthMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - - if (!message.data.bandwidthData) { - logger.warn("No bandwidth data provided"); - } - - const bandwidthData: PeerBandwidth[] = message.data.bandwidthData; - - if (!Array.isArray(bandwidthData)) { - throw new Error("Invalid bandwidth data"); - } - - await db.transaction(async (trx) => { - for (const peer of bandwidthData) { - const { publicKey, bytesIn, bytesOut } = peer; - - // Find the client by public key - const [client] = await trx - .select() - .from(clients) - .where(eq(clients.pubKey, publicKey)) - .limit(1); - - if (!client) { - continue; - } - - // Update the client's bandwidth usage - await trx - .update(clients) - .set({ - megabytesOut: (client.megabytesIn || 0) + bytesIn, - megabytesIn: (client.megabytesOut || 0) + bytesOut, - lastBandwidthUpdate: new Date().toISOString(), - }) - .where(eq(clients.clientId, client.clientId)); - } - }); -}; diff --git a/server/routers/newt/handleRegisterMessage.ts b/server/routers/newt/handleRegisterMessage.ts new file mode 100644 index 00000000..bf64e3ec --- /dev/null +++ b/server/routers/newt/handleRegisterMessage.ts @@ -0,0 +1,174 @@ +import db from "@server/db"; +import { MessageHandler } from "../ws"; +import { + exitNodes, + resources, + sites, + Target, + targets +} from "@server/db/schemas"; +import { eq, and, sql, inArray } from "drizzle-orm"; +import { addPeer, deletePeer } from "../gerbil/peers"; +import logger from "@server/logger"; + +export const handleRegisterMessage: MessageHandler = async (context) => { + const { message, newt, sendToClient } = context; + + logger.info("Handling register message!"); + + if (!newt) { + logger.warn("Newt not found"); + return; + } + + if (!newt.siteId) { + logger.warn("Newt has no site!"); // TODO: Maybe we create the site here? + return; + } + + const siteId = newt.siteId; + + const { publicKey } = message.data; + if (!publicKey) { + logger.warn("Public key not provided"); + return; + } + + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (!site || !site.exitNodeId) { + logger.warn("Site not found or does not have exit node"); + return; + } + + await db + .update(sites) + .set({ + pubKey: publicKey + }) + .where(eq(sites.siteId, siteId)) + .returning(); + + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)) + .limit(1); + + if (site.pubKey && site.pubKey !== publicKey) { + logger.info("Public key mismatch. Deleting old peer..."); + await deletePeer(site.exitNodeId, site.pubKey); + } + + if (!site.subnet) { + logger.warn("Site has no subnet"); + return; + } + + // add the peer to the exit node + await addPeer(site.exitNodeId, { + publicKey: publicKey, + allowedIps: [site.subnet] + }); + + // Improved version + const allResources = await db.transaction(async (tx) => { + // First get all resources for the site + const resourcesList = await tx + .select({ + resourceId: resources.resourceId, + subdomain: resources.subdomain, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + blockAccess: resources.blockAccess, + sso: resources.sso, + emailWhitelistEnabled: resources.emailWhitelistEnabled, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol + }) + .from(resources) + .where(eq(resources.siteId, siteId)); + + // Get all enabled targets for these resources in a single query + const resourceIds = resourcesList.map((r) => r.resourceId); + const allTargets = + resourceIds.length > 0 + ? await tx + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled + }) + .from(targets) + .where( + and( + inArray(targets.resourceId, resourceIds), + eq(targets.enabled, true) + ) + ) + : []; + + // Combine the data in JS instead of using SQL for the JSON + return resourcesList.map((resource) => ({ + ...resource, + targets: allTargets.filter( + (target) => target.resourceId === resource.resourceId + ) + })); + }); + + const { tcpTargets, udpTargets } = allResources.reduce( + (acc, resource) => { + // Skip resources with no targets + if (!resource.targets?.length) return acc; + + // Format valid targets into strings + const formattedTargets = resource.targets + .filter( + (target: Target) => + target?.internalPort && target?.ip && target?.port + ) + .map( + (target: Target) => + `${target.internalPort}:${target.ip}:${target.port}` + ); + + // Add to the appropriate protocol array + if (resource.protocol === "tcp") { + acc.tcpTargets.push(...formattedTargets); + } else { + acc.udpTargets.push(...formattedTargets); + } + + return acc; + }, + { tcpTargets: [] as string[], udpTargets: [] as string[] } + ); + + return { + message: { + type: "newt/wg/connect", + data: { + endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`, + publicKey: exitNode.publicKey, + serverIP: exitNode.address.split("/")[0], + tunnelIP: site.subnet.split("/")[0], + targets: { + udp: udpTargets, + tcp: tcpTargets + } + } + }, + broadcast: false, // Send to all clients + excludeSender: false // Include sender in broadcast + }; +}; diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts deleted file mode 100644 index 0491393f..00000000 --- a/server/routers/newt/handleSocketMessages.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MessageHandler } from "@server/routers/ws"; -import logger from "@server/logger"; -import { dockerSocketCache } from "./dockerSocket"; -import { Newt } from "@server/db"; -import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint"; - -export const handleDockerStatusMessage: MessageHandler = async (context) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - logger.info("Handling Docker socket check response"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - logger.info(`Newt ID: ${newt.newtId}, Site ID: ${newt.siteId}`); - const { available, socketPath } = message.data; - - logger.info( - `Docker socket availability for Newt ${newt.newtId}: available=${available}, socketPath=${socketPath}` - ); - - if (available) { - logger.info(`Newt ${newt.newtId} has Docker socket access`); - dockerSocketCache.set(`${newt.newtId}:socketPath`, socketPath, 0); - dockerSocketCache.set(`${newt.newtId}:isAvailable`, available, 0); - } else { - logger.warn(`Newt ${newt.newtId} does not have Docker socket access`); - } - - return; -}; - -export const handleDockerContainersMessage: MessageHandler = async ( - context -) => { - const { message, client, sendToClient } = context; - const newt = client as Newt; - - logger.info("Handling Docker containers response"); - - if (!newt) { - logger.warn("Newt not found"); - return; - } - - logger.info(`Newt ID: ${newt.newtId}, Site ID: ${newt.siteId}`); - const { containers } = message.data; - - logger.info( - `Docker containers for Newt ${newt.newtId}: ${containers ? containers.length : 0}` - ); - - if (containers && containers.length > 0) { - dockerSocketCache.set(`${newt.newtId}:dockerContainers`, containers, 0); - } else { - logger.warn(`Newt ${newt.newtId} does not have Docker containers`); - } - - if (!newt.siteId) { - logger.warn("Newt has no site!"); - return; - } - - await applyNewtDockerBlueprint( - newt.siteId, - newt.newtId, - containers - ); -}; diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 9642a637..dcc49749 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -1,8 +1,3 @@ export * from "./createNewt"; -export * from "./getNewtToken"; -export * from "./handleNewtRegisterMessage"; -export * from "./handleReceiveBandwidthMessage"; -export * from "./handleGetConfigMessage"; -export * from "./handleSocketMessages"; -export * from "./handleNewtPingRequestMessage"; -export * from "./handleApplyBlueprintMessage"; \ No newline at end of file +export * from "./getToken"; +export * from "./handleRegisterMessage"; \ No newline at end of file diff --git a/server/routers/newt/peers.ts b/server/routers/newt/peers.ts deleted file mode 100644 index 0c0765a5..00000000 --- a/server/routers/newt/peers.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { db } from "@server/db"; -import { newts, sites } from "@server/db"; -import { eq } from "drizzle-orm"; -import { sendToClient } from "@server/routers/ws"; -import logger from "@server/logger"; - -export async function addPeer( - siteId: number, - peer: { - publicKey: string; - allowedIps: string[]; - endpoint: string; - } -) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - if (!site) { - throw new Error(`Exit node with ID ${siteId} not found`); - } - - // get the newt on the site - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, siteId)) - .limit(1); - if (!newt) { - throw new Error(`Site found for site ${siteId}`); - } - - sendToClient(newt.newtId, { - type: "newt/wg/peer/add", - data: peer - }); - - logger.info(`Added peer ${peer.publicKey} to newt ${newt.newtId}`); - - return site; -} - -export async function deletePeer(siteId: number, publicKey: string) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - if (!site) { - throw new Error(`Site with ID ${siteId} not found`); - } - - // get the newt on the site - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, siteId)) - .limit(1); - if (!newt) { - throw new Error(`Newt not found for site ${siteId}`); - } - - sendToClient(newt.newtId, { - type: "newt/wg/peer/remove", - data: { - publicKey - } - }); - - logger.info(`Deleted peer ${publicKey} from newt ${newt.newtId}`); - - return site; -} - -export async function updatePeer( - siteId: number, - publicKey: string, - peer: { - allowedIps?: string[]; - endpoint?: string; - } -) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - if (!site) { - throw new Error(`Site with ID ${siteId} not found`); - } - - // get the newt on the site - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, siteId)) - .limit(1); - if (!newt) { - throw new Error(`Newt not found for site ${siteId}`); - } - - sendToClient(newt.newtId, { - type: "newt/wg/peer/update", - data: { - publicKey, - ...peer - } - }); - - logger.info(`Updated peer ${publicKey} on newt ${newt.newtId}`); - - return site; -} diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index bf8d7290..f2f5dc45 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,14 +1,10 @@ -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"; +import { Target } from "@server/db/schemas"; +import { sendToClient } from "../ws"; -export async function addTargets( +export function addTargets( newtId: string, targets: Target[], - healthCheckData: TargetHealthCheck[], - protocol: string, - port: number | null = null + protocol: string ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -17,75 +13,19 @@ export async function addTargets( }:${target.port}`; }); - await sendToClient(newtId, { + const payload = { type: `newt/${protocol}/add`, data: { 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 - } - }); + }; + sendToClient(newtId, payload); } -export async function removeTargets( +export function removeTargets( newtId: string, targets: Target[], - protocol: string, - port: number | null = null + protocol: string ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -94,21 +34,11 @@ export async function removeTargets( }:${target.port}`; }); - await sendToClient(newtId, { + const payload = { type: `newt/${protocol}/remove`, data: { targets: payloadTargets } - }); - - const healthCheckTargets = targets.map((target) => { - return target.targetId; - }); - - await sendToClient(newtId, { - type: `newt/healthcheck/remove`, - data: { - ids: healthCheckTargets - } - }); + }; + sendToClient(newtId, payload); } diff --git a/server/routers/olm/createOlm.ts b/server/routers/olm/createOlm.ts deleted file mode 100644 index 64b9c932..00000000 --- a/server/routers/olm/createOlm.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { db } from "@server/db"; -import { hash } from "@node-rs/argon2"; -import HttpCode from "@server/types/HttpCode"; -import { z } from "zod"; -import { newts } from "@server/db"; -import createHttpError from "http-errors"; -import response from "@server/lib/response"; -import { SqliteError } from "better-sqlite3"; -import moment from "moment"; -import { generateSessionToken } from "@server/auth/sessions/app"; -import { createNewtSession } from "@server/auth/sessions/newt"; -import { fromError } from "zod-validation-error"; -import { hashPassword } from "@server/auth/password"; - -export const createNewtBodySchema = z.object({}); - -export type CreateNewtBody = z.infer; - -export type CreateNewtResponse = { - token: string; - newtId: string; - secret: string; -}; - -const createNewtSchema = z - .object({ - newtId: z.string(), - secret: z.string() - }) - .strict(); - -export async function createNewt( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - - const parsedBody = createNewtSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { newtId, secret } = parsedBody.data; - - if (req.user && (!req.userRoleIds || req.userRoleIds.length === 0)) { - return next( - createHttpError(HttpCode.FORBIDDEN, "User does not have a role") - ); - } - - const secretHash = await hashPassword(secret); - - await db.insert(newts).values({ - newtId: newtId, - secretHash, - dateCreated: moment().toISOString(), - }); - - // give the newt their default permissions: - // await db.insert(newtActions).values({ - // newtId: newtId, - // actionId: ActionsEnum.createOrg, - // orgId: null, - // }); - - const token = generateSessionToken(); - await createNewtSession(token, newtId); - - return response(res, { - data: { - newtId, - secret, - token, - }, - success: true, - error: false, - message: "Newt created successfully", - status: HttpCode.OK, - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "A newt with that email address already exists" - ) - ); - } else { - console.error(e); - - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create newt" - ) - ); - } - } -} diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts deleted file mode 100644 index c26f5936..00000000 --- a/server/routers/olm/getOlmToken.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { olms } from "@server/db"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { eq } from "drizzle-orm"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import { - createOlmSession, - validateOlmSessionToken -} from "@server/auth/sessions/olm"; -import { verifyPassword } from "@server/auth/password"; -import logger from "@server/logger"; -import config from "@server/lib/config"; - -export const olmGetTokenBodySchema = z.object({ - olmId: z.string(), - secret: z.string(), - token: z.string().optional() -}); - -export type OlmGetTokenBody = z.infer; - -export async function getOlmToken( - req: Request, - res: Response, - next: NextFunction -): Promise { - const parsedBody = olmGetTokenBodySchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { olmId, secret, token } = parsedBody.data; - - try { - if (token) { - const { session, olm } = await validateOlmSessionToken(token); - if (session) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Olm session already valid. Olm ID: ${olmId}. IP: ${req.ip}.` - ); - } - return response(res, { - data: null, - success: true, - error: false, - message: "Token session already valid", - status: HttpCode.OK - }); - } - } - - const existingOlmRes = await db - .select() - .from(olms) - .where(eq(olms.olmId, olmId)); - if (!existingOlmRes || !existingOlmRes.length) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "No olm found with that olmId" - ) - ); - } - - const existingOlm = existingOlmRes[0]; - - const validSecret = await verifyPassword( - secret, - existingOlm.secretHash - ); - if (!validSecret) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.` - ); - } - return next( - createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect") - ); - } - - logger.debug("Creating new olm session token"); - - const resToken = generateSessionToken(); - await createOlmSession(resToken, existingOlm.olmId); - - logger.debug("Token created successfully"); - - return response<{ token: string }>(res, { - data: { - token: resToken - }, - success: true, - error: false, - message: "Token created successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to authenticate olm" - ) - ); - } -} diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts deleted file mode 100644 index 6f00640d..00000000 --- a/server/routers/olm/handleOlmPingMessage.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { db } from "@server/db"; -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"; - -// Track if the offline checker interval is running -let offlineCheckerInterval: NodeJS.Timeout | null = null; -const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds -const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes - -/** - * Starts the background interval that checks for clients that haven't pinged recently - * and marks them as offline - */ -export const startOlmOfflineChecker = (): void => { - if (offlineCheckerInterval) { - return; // Already running - } - - offlineCheckerInterval = setInterval(async () => { - try { - const twoMinutesAgo = Math.floor((Date.now() - OFFLINE_THRESHOLD_MS) / 1000); - - // Find clients that haven't pinged in the last 2 minutes and mark them as offline - await db - .update(clients) - .set({ online: false }) - .where( - and( - eq(clients.online, true), - or( - lt(clients.lastPing, twoMinutesAgo), - isNull(clients.lastPing) - ) - ) - ); - - } catch (error) { - logger.error("Error in offline checker interval", { error }); - } - }, OFFLINE_CHECK_INTERVAL); - - logger.info("Started offline checker interval"); -}; - -/** - * Stops the background interval that checks for offline clients - */ -export const stopOlmOfflineChecker = (): void => { - if (offlineCheckerInterval) { - clearInterval(offlineCheckerInterval); - offlineCheckerInterval = null; - logger.info("Stopped offline checker interval"); - } -}; - -/** - * Handles ping messages from clients and responds with pong - */ -export const handleOlmPingMessage: MessageHandler = async (context) => { - const { message, client: c, sendToClient } = context; - const olm = c as Olm; - - if (!olm) { - logger.warn("Olm not found"); - return; - } - - if (!olm.clientId) { - logger.warn("Olm has no client ID!"); - return; - } - - try { - // Update the client's last ping timestamp - await db - .update(clients) - .set({ - lastPing: Math.floor(Date.now() / 1000), - online: true, - }) - .where(eq(clients.clientId, olm.clientId)); - } catch (error) { - logger.error("Error handling ping message", { error }); - } - - return { - message: { - type: "pong", - data: { - timestamp: new Date().toISOString(), - } - }, - broadcast: false, - excludeSender: false - }; -}; diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts deleted file mode 100644 index fdae084d..00000000 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { db, ExitNode } from "@server/db"; -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"; -import logger from "@server/logger"; -import { listExitNodes } from "@server/lib/exitNodes"; - -export const handleOlmRegisterMessage: MessageHandler = async (context) => { - logger.info("Handling register olm message!"); - const { message, client: c, sendToClient } = context; - const olm = c as Olm; - - const now = new Date().getTime() / 1000; - - if (!olm) { - logger.warn("Olm not found"); - return; - } - if (!olm.clientId) { - logger.warn("Olm has no client ID!"); - return; - } - const clientId = olm.clientId; - const { publicKey, relay, olmVersion } = message.data; - - logger.debug( - `Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}` - ); - - if (!publicKey) { - logger.warn("Public key not provided"); - return; - } - - // Get the client - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (!client) { - logger.warn("Client not found"); - return; - } - - if (client.exitNodeId) { - // TODO: FOR NOW WE ARE JUST HOLEPUNCHING ALL EXIT NODES BUT IN THE FUTURE WE SHOULD HANDLE THIS BETTER - - // Get the exit node - const allExitNodes = await listExitNodes(client.orgId, true); // FILTER THE ONLINE ONES - - const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { - return { - publicKey: exitNode.publicKey, - endpoint: exitNode.endpoint - }; - }); - - // Send holepunch message - await sendToClient(olm.olmId, { - type: "olm/wg/holepunch/all", - data: { - exitNodes: exitNodesHpData - } - }); - - if (!olmVersion) { - // THIS IS FOR BACKWARDS COMPATIBILITY - // THE OLDER CLIENTS DID NOT SEND THE VERSION - await sendToClient(olm.olmId, { - type: "olm/wg/holepunch", - data: { - serverPubKey: allExitNodes[0].publicKey, - endpoint: allExitNodes[0].endpoint - } - }); - } - } - - if (olmVersion) { - await db - .update(olms) - .set({ - version: olmVersion - }) - .where(eq(olms.olmId, olm.olmId)); - } - - // if (now - (client.lastHolePunch || 0) > 6) { - // logger.warn("Client last hole punch is too old, skipping all sites"); - // return; - // } - - if (client.pubKey !== publicKey) { - logger.info( - "Public key mismatch. Updating public key and clearing session info..." - ); - // Update the client's public key - await db - .update(clients) - .set({ - pubKey: publicKey - }) - .where(eq(clients.clientId, olm.clientId)); - - // set isRelay to false for all of the client's sites to reset the connection metadata - await db - .update(clientSites) - .set({ - isRelayed: relay == true - }) - .where(eq(clientSites.clientId, olm.clientId)); - } - - // Get all sites data - const sitesData = await db - .select() - .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .where(eq(clientSites.clientId, client.clientId)); - - // Prepare an array to store site configurations - const siteConfigurations = []; - logger.debug( - `Found ${sitesData.length} sites for client ${client.clientId}` - ); - - if (sitesData.length === 0) { - sendToClient(olm.olmId, { - type: "olm/register/no-sites", - data: {} - }); - } - - // Process each site - for (const { sites: site } of sitesData) { - if (!site.exitNodeId) { - logger.warn( - `Site ${site.siteId} does not have exit node, skipping` - ); - continue; - } - - // Validate endpoint and hole punch status - if (!site.endpoint) { - logger.warn(`In olm register: site ${site.siteId} has no endpoint, skipping`); - continue; - } - - // if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { - // logger.warn( - // `Site ${site.siteId} last hole punch is too old, skipping` - // ); - // continue; - // } - - // If public key changed, delete old peer from this site - if (client.pubKey && client.pubKey != publicKey) { - logger.info( - `Public key mismatch. Deleting old peer from site ${site.siteId}...` - ); - await deletePeer(site.siteId, client.pubKey!); - } - - if (!site.subnet) { - logger.warn(`Site ${site.siteId} has no subnet, skipping`); - continue; - } - - const [clientSite] = await db - .select() - .from(clientSites) - .where( - and( - eq(clientSites.clientId, client.clientId), - eq(clientSites.siteId, site.siteId) - ) - ) - .limit(1); - - // Add the peer to the exit node for this site - if (clientSite.endpoint) { - logger.info( - `Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}` - ); - await addPeer(site.siteId, { - publicKey: publicKey, - allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client - endpoint: relay ? "" : clientSite.endpoint - }); - } else { - logger.warn( - `Client ${client.clientId} has no endpoint, skipping peer addition` - ); - } - - let endpoint = site.endpoint; - if (relay) { - 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}`); - continue; - } - endpoint = `${exitNode.endpoint}:21820`; - } - - // Add site configuration to the array - siteConfigurations.push({ - siteId: site.siteId, - endpoint: endpoint, - publicKey: site.publicKey, - serverIP: site.address, - serverPort: site.listenPort, - remoteSubnets: site.remoteSubnets - }); - } - - // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES - // if (siteConfigurations.length === 0) { - // logger.warn("No valid site configurations found"); - // return; - // } - - // Return connect message with all site configurations - return { - message: { - type: "olm/wg/connect", - data: { - sites: siteConfigurations, - tunnelIP: client.subnet - } - }, - broadcast: false, - excludeSender: false - }; -}; diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts deleted file mode 100644 index 9b31754c..00000000 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { db, exitNodes, sites } from "@server/db"; -import { MessageHandler } from "@server/routers/ws"; -import { clients, clientSites, Olm } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import { updatePeer } from "../newt/peers"; -import logger from "@server/logger"; - -export const handleOlmRelayMessage: MessageHandler = async (context) => { - const { message, client: c, sendToClient } = context; - const olm = c as Olm; - - logger.info("Handling relay olm message!"); - - if (!olm) { - logger.warn("Olm not found"); - return; - } - - if (!olm.clientId) { - logger.warn("Olm has no site!"); // TODO: Maybe we create the site here? - return; - } - - const clientId = olm.clientId; - - const [client] = await db - .select() - .from(clients) - .where(eq(clients.clientId, clientId)) - .limit(1); - - if (!client) { - logger.warn("Client not found"); - return; - } - - // make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old - if (!client.pubKey) { - logger.warn("Client has no endpoint or listen port"); - return; - } - - const { siteId } = message.data; - - // Get the site - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!site || !site.exitNodeId) { - logger.warn("Site not found or has no exit node"); - return; - } - - // get the site's exit node - 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"); - return; - } - - await db - .update(clientSites) - .set({ - isRelayed: true - }) - .where( - and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, siteId) - ) - ); - - // update the peer on the exit node - await updatePeer(siteId, client.pubKey, { - endpoint: "" // this removes the endpoint - }); - - sendToClient(olm.olmId, { - type: "olm/wg/peer/relay", - data: { - siteId: siteId, - endpoint: exitNode.endpoint, - publicKey: exitNode.publicKey - } - }); - - return; -}; diff --git a/server/routers/olm/index.ts b/server/routers/olm/index.ts deleted file mode 100644 index 8426612e..00000000 --- a/server/routers/olm/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./handleOlmRegisterMessage"; -export * from "./getOlmToken"; -export * from "./createOlm"; -export * from "./handleOlmRelayMessage"; -export * from "./handleOlmPingMessage"; \ No newline at end of file diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts deleted file mode 100644 index ab592bdd..00000000 --- a/server/routers/olm/peers.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { db } from "@server/db"; -import { clients, olms, newts, sites } from "@server/db"; -import { eq } from "drizzle-orm"; -import { sendToClient } from "@server/routers/ws"; -import logger from "@server/logger"; - -export async function addPeer( - clientId: number, - peer: { - siteId: number; - publicKey: string; - endpoint: string; - serverIP: string | null; - serverPort: number | null; - remoteSubnets: string | null; // optional, comma-separated list of subnets that this site can access - } -) { - const [olm] = await db - .select() - .from(olms) - .where(eq(olms.clientId, clientId)) - .limit(1); - if (!olm) { - throw new Error(`Olm with ID ${clientId} not found`); - } - - await sendToClient(olm.olmId, { - type: "olm/wg/peer/add", - data: { - siteId: peer.siteId, - publicKey: peer.publicKey, - endpoint: peer.endpoint, - serverIP: peer.serverIP, - serverPort: peer.serverPort, - remoteSubnets: peer.remoteSubnets // optional, comma-separated list of subnets that this site can access - } - }); - - logger.info(`Added peer ${peer.publicKey} to olm ${olm.olmId}`); -} - -export async function deletePeer(clientId: number, siteId: number, publicKey: string) { - const [olm] = await db - .select() - .from(olms) - .where(eq(olms.clientId, clientId)) - .limit(1); - if (!olm) { - throw new Error(`Olm with ID ${clientId} not found`); - } - - await sendToClient(olm.olmId, { - type: "olm/wg/peer/remove", - data: { - publicKey, - siteId: siteId - } - }); - - logger.info(`Deleted peer ${publicKey} from olm ${olm.olmId}`); -} - -export async function updatePeer( - clientId: number, - peer: { - siteId: number; - publicKey: string; - endpoint: string; - serverIP: string | null; - serverPort: number | null; - remoteSubnets?: string | null; // optional, comma-separated list of subnets that - } -) { - const [olm] = await db - .select() - .from(olms) - .where(eq(olms.clientId, clientId)) - .limit(1); - if (!olm) { - throw new Error(`Olm with ID ${clientId} not found`); - } - - await sendToClient(olm.olmId, { - type: "olm/wg/peer/update", - data: { - siteId: peer.siteId, - publicKey: peer.publicKey, - endpoint: peer.endpoint, - serverIP: peer.serverIP, - serverPort: peer.serverPort, - remoteSubnets: peer.remoteSubnets - } - }); - - logger.info(`Added peer ${peer.publicKey} to olm ${olm.olmId}`); -} diff --git a/server/routers/org/applyBlueprint.ts b/server/routers/org/applyBlueprint.ts deleted file mode 100644 index 982258ee..00000000 --- a/server/routers/org/applyBlueprint.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { eq } from "drizzle-orm"; -import { - apiKeyOrg, - apiKeys, - domains, - Org, - orgDomains, - orgs, - roleActions, - roles, - userOrgs, - users, - actions -} from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import config from "@server/lib/config"; -import { fromError } from "zod-validation-error"; -import { defaultRoleAllowedActions } from "../role"; -import { OpenAPITags, registry } from "@server/openApi"; -import { isValidCIDR } from "@server/lib/validators"; -import { applyBlueprint as applyBlueprintFunc } from "@server/lib/blueprints/applyBlueprint"; - -const applyBlueprintSchema = z - .object({ - blueprint: z.string() - }) - .strict(); - -const applyBlueprintParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -registry.registerPath({ - method: "put", - path: "/org/{orgId}/blueprint", - description: "Apply a base64 encoded blueprint to an organization", - tags: [OpenAPITags.Org], - request: { - params: applyBlueprintParamsSchema, - body: { - content: { - "application/json": { - schema: applyBlueprintSchema - } - } - } - }, - responses: {} -}); - -export async function applyBlueprint( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = applyBlueprintParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - - const parsedBody = applyBlueprintSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { blueprint } = parsedBody.data; - - if (!blueprint) { - logger.warn("No blueprint provided"); - return; - } - - logger.debug(`Received blueprint: ${blueprint}`); - - try { - // first base64 decode the blueprint - const decoded = Buffer.from(blueprint, "base64").toString("utf-8"); - // then parse the json - const blueprintParsed = JSON.parse(decoded); - - // Update the blueprint in the database - await applyBlueprintFunc(orgId, blueprintParsed); - } catch (error) { - logger.error(`Failed to update database from config: ${error}`); - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Failed to update database from config: ${error}` - ) - ); - } - - return response(res, { - data: null, - success: true, - error: false, - message: "Blueprint applied successfully", - status: HttpCode.CREATED - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/org/checkId.ts b/server/routers/org/checkId.ts index c5d00002..40a347aa 100644 --- a/server/routers/org/checkId.ts +++ b/server/routers/org/checkId.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs } from "@server/db"; +import { orgs } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index acfa94a5..60ff5558 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -3,6 +3,8 @@ import { z } from "zod"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import { + apiKeyOrg, + apiKeys, domains, Org, orgDomains, @@ -10,27 +12,27 @@ import { roleActions, roles, userOrgs, - users, - actions -} from "@server/db"; + users +} from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; +import { createAdminRole } from "@server/setup/ensureActions"; import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; import { defaultRoleAllowedActions } from "../role"; import { OpenAPITags, registry } from "@server/openApi"; -import { isValidCIDR } from "@server/lib/validators"; const createOrgSchema = z .object({ orgId: z.string(), - name: z.string().min(1).max(255), - subnet: z.string() + name: z.string().min(1).max(255) }) .strict(); +// const MAX_ORGS = 5; + registry.registerPath({ method: "put", path: "/org", @@ -76,34 +78,18 @@ export async function createOrg( ); } - const { orgId, name, subnet } = parsedBody.data; - - if (!isValidCIDR(subnet)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid subnet format. Please provide a valid CIDR notation." - ) - ); - } - - // TODO: for now we are making all of the orgs the same subnet - // make sure the subnet is unique - // const subnetExists = await db - // .select() - // .from(orgs) - // .where(eq(orgs.subnet, subnet)) - // .limit(1); - - // if (subnetExists.length > 0) { + // const userOrgIds = req.userOrgIds; + // if (userOrgIds && userOrgIds.length > MAX_ORGS) { // return next( // createHttpError( - // HttpCode.CONFLICT, - // `Subnet ${subnet} already exists` + // HttpCode.FORBIDDEN, + // `Maximum number of organizations reached.` // ) // ); // } + const { orgId, name } = parsedBody.data; + // make sure the orgId is unique const orgExists = await db .select() @@ -133,9 +119,7 @@ export async function createOrg( .insert(orgs) .values({ orgId, - name, - subnet, - createdAt: new Date().toISOString() + name }) .returning(); @@ -147,46 +131,20 @@ export async function createOrg( 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 }); + const roleId = await createAdminRole(newOrg[0].orgId); - if (!insertedRole || !insertedRole.roleId) { + if (!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(orgDomains).values( + allDomains.map((domain) => ({ + orgId: newOrg[0].orgId, + domainId: domain.domainId + })) + ); if (req.user) { await trx.insert(userOrgs).values({ @@ -232,13 +190,25 @@ export async function createOrg( orgId })) ); + + const rootApiKeys = await trx + .select() + .from(apiKeys) + .where(eq(apiKeys.isRoot, true)); + + for (const apiKey of rootApiKeys) { + await trx.insert(apiKeyOrg).values({ + apiKeyId: apiKey.apiKeyId, + orgId: newOrg[0].orgId + }); + } }); if (!org) { return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create org" + "Failed to createo org" ) ); } diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index a2b0ecd8..030588c5 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -1,15 +1,21 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, domains, orgDomains, resources } from "@server/db"; -import { newts, newtSessions, orgs, sites, userActions } from "@server/db"; -import { eq, and, inArray, sql } from "drizzle-orm"; +import { db } from "@server/db"; +import { + newts, + newtSessions, + orgs, + sites, + userActions +} from "@server/db/schemas"; +import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; 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 "@server/routers/ws"; +import { sendToClient } from "../ws"; import { deletePeer } from "../gerbil/peers"; import { OpenAPITags, registry } from "@server/openApi"; @@ -49,7 +55,19 @@ export async function deleteOrg( } const { orgId } = parsedParams.data; - + // Check if the user has permission to list sites + const hasPermission = await checkUserActionPermission( + ActionsEnum.deleteOrg, + req + ); + if (!hasPermission) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have permission to perform this action" + ) + ); + } const [org] = await db .select() .from(orgs) @@ -71,8 +89,6 @@ export async function deleteOrg( .where(eq(sites.orgId, orgId)) .limit(1); - const deletedNewtIds: string[] = []; - await db.transaction(async (trx) => { if (sites) { for (const site of orgSites) { @@ -86,7 +102,11 @@ export async function deleteOrg( .where(eq(newts.siteId, site.siteId)) .returning(); if (deletedNewt) { - deletedNewtIds.push(deletedNewt.newtId); + const payload = { + type: `newt/terminate`, + data: {} + }; + sendToClient(deletedNewt.newtId, payload); // delete all of the sessions for the newt await trx @@ -108,62 +128,9 @@ export async function deleteOrg( } } - const allOrgDomains = await trx - .select() - .from(orgDomains) - .innerJoin(domains, eq(domains.domainId, orgDomains.domainId)) - .where( - and( - eq(orgDomains.orgId, orgId), - eq(domains.configManaged, false) - ) - ); - - // For each domain, check if it belongs to multiple organizations - const domainIdsToDelete: string[] = []; - for (const orgDomain of allOrgDomains) { - const domainId = orgDomain.domains.domainId; - - // Count how many organizations this domain belongs to - const orgCount = await trx - .select({ count: sql`count(*)` }) - .from(orgDomains) - .where(eq(orgDomains.domainId, domainId)); - - // Only delete the domain if it belongs to exactly 1 organization (the one being deleted) - if (orgCount[0].count === 1) { - domainIdsToDelete.push(domainId); - } - } - - // Delete domains that belong exclusively to this organization - if (domainIdsToDelete.length > 0) { - await trx - .delete(domains) - .where(inArray(domains.domainId, domainIdsToDelete)); - } - - // Delete resources - await trx.delete(resources).where(eq(resources.orgId, orgId)); - await trx.delete(orgs).where(eq(orgs.orgId, orgId)); }); - // Send termination messages outside of transaction to prevent blocking - for (const newtId of deletedNewtIds) { - const payload = { - type: `newt/terminate`, - data: {} - }; - // Don't await this to prevent blocking the response - sendToClient(newtId, payload).catch((error) => { - logger.error( - "Failed to send termination message to newt:", - error - ); - }); - } - return response(res, { data: null, success: true, diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 89c77f13..c112ab7a 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { Org, orgs } from "@server/db"; +import { Org, orgs } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -17,7 +17,7 @@ const getOrgSchema = z .strict(); export type GetOrgResponse = { - org: Org & { settings: { } | null }; + org: Org; }; registry.registerPath({ @@ -64,23 +64,9 @@ 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], - settings: parsedSettings - } + org: org[0] }, success: true, error: false, diff --git a/server/routers/org/getOrgOverview.ts b/server/routers/org/getOrgOverview.ts index 15131a21..59ae08f8 100644 --- a/server/routers/org/getOrgOverview.ts +++ b/server/routers/org/getOrgOverview.ts @@ -10,7 +10,7 @@ import { userResources, users, userSites -} from "@server/db"; +} from "@server/db/schemas"; import { and, count, eq, inArray, countDistinct } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index 7887fcac..5623823d 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -6,5 +6,3 @@ export * from "./listUserOrgs"; export * from "./checkId"; export * from "./getOrgOverview"; export * from "./listOrgs"; -export * from "./pickOrgDefaults"; -export * from "./applyBlueprint"; diff --git a/server/routers/org/listOrgs.ts b/server/routers/org/listOrgs.ts index 07705e48..27114104 100644 --- a/server/routers/org/listOrgs.ts +++ b/server/routers/org/listOrgs.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { Org, orgs, userOrgs } from "@server/db"; +import { Org, orgs, userOrgs } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index 0e179eea..fa33d2cb 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { Org, orgs, userOrgs } from "@server/db"; +import { Org, orgs, userOrgs } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { sql, inArray, eq, and } from "drizzle-orm"; +import { sql, inArray, eq } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -40,10 +40,8 @@ const listOrgsSchema = z.object({ // responses: {} // }); -type ResponseOrg = Org & { isOwner?: boolean }; - export type ListUserOrgsResponse = { - orgs: ResponseOrg[]; + orgs: Org[]; pagination: { total: number; limit: number; offset: number }; }; @@ -105,20 +103,7 @@ export async function listUserOrgs( } const organizations = await db - .select({ - orgId: orgs.orgId, - name: orgs.name, - subnet: orgs.subnet, - createdAt: orgs.createdAt, - settings: orgs.settings, - isOwner: sql` - exists (select 1 - from ${userOrgs} g - where g.userId = ${userId} - and g.orgId = ${orgs.orgId} - and g.isOwner) - ` - }) + .select() .from(orgs) .where(inArray(orgs.orgId, userOrgIds)) .limit(limit) diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts deleted file mode 100644 index 771b0d99..00000000 --- a/server/routers/org/pickOrgDefaults.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { getNextAvailableOrgSubnet } from "@server/lib/ip"; -import config from "@server/lib/config"; - -export type PickOrgDefaultsResponse = { - subnet: string; -}; - -export async function pickOrgDefaults( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - // TODO: Why would each org have to have its own subnet? - // const subnet = await getNextAvailableOrgSubnet(); - // Just hard code the subnet for now for everyone - const subnet = config.getRawConfig().orgs.subnet_group; - - return response(res, { - data: { - subnet: subnet - }, - success: true, - error: false, - message: "Organization defaults created successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 6f30e62c..0f0aa89a 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs } from "@server/db"; +import { orgs } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -12,15 +12,14 @@ 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(), - settings: z.object({ - }).optional(), + name: z.string().min(1).max(255).optional() + // domain: z.string().min(1).max(255).optional(), }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -72,15 +71,11 @@ export async function updateOrg( } const { orgId } = parsedParams.data; - - const settings = parsedBody.data.settings ? JSON.stringify(parsedBody.data.settings) : undefined; + const updateData = parsedBody.data; const updatedOrg = await db .update(orgs) - .set({ - name: parsedBody.data.name, - settings: settings - }) + .set(updateData) .where(eq(orgs.orgId, orgId)) .returning(); diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts deleted file mode 100644 index a8e205cc..00000000 --- a/server/routers/orgIdp/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 55d0a286..00000000 --- a/server/routers/remoteExitNode/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -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/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index 2d7fdf93..961b2d8a 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -1,6 +1,6 @@ import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { Resource, resources } from "@server/db"; +import db from "@server/db"; +import { Resource, resources } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index 652c4e86..602ddccd 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -1,7 +1,7 @@ import { verify } from "@node-rs/argon2"; import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { orgs, resourcePassword, resources } from "@server/db"; +import db from "@server/db"; +import { orgs, resourcePassword, resources } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index d8733c18..21640942 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -1,6 +1,6 @@ import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; -import { orgs, resourcePincode, resources } from "@server/db"; +import db from "@server/db"; +import { orgs, resourcePincode, resources } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq } from "drizzle-orm"; diff --git a/server/routers/resource/authWithWhitelist.ts b/server/routers/resource/authWithWhitelist.ts index 07662f7f..01c9909c 100644 --- a/server/routers/resource/authWithWhitelist.ts +++ b/server/routers/resource/authWithWhitelist.ts @@ -1,11 +1,11 @@ import { generateSessionToken } from "@server/auth/sessions/app"; -import { db } from "@server/db"; +import db from "@server/db"; import { orgs, resourceOtp, resources, resourceWhitelist -} from "@server/db"; +} from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq, and } from "drizzle-orm"; @@ -22,8 +22,8 @@ const authWithWhitelistBodySchema = z .object({ email: z .string() - .toLowerCase() - .email(), + .email() + .transform((v) => v.toLowerCase()), otp: z.string().optional() }) .strict(); diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 759a432f..35dc4bf6 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -10,21 +10,21 @@ import { roleResources, roles, userResources -} from "@server/db"; +} from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { eq, and } from "drizzle-orm"; +import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { subdomainSchema } from "@server/lib/schemas"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; -import { getUniqueResourceName } from "@server/db/names"; -import { validateAndConstructDomain } from "@server/lib/domainUtils"; const createResourceParamsSchema = z .object({ + siteId: z.string().transform(stoi).pipe(z.number().int().positive()), orgId: z.string() }) .strict(); @@ -32,11 +32,15 @@ const createResourceParamsSchema = z const createHttpResourceSchema = z .object({ name: z.string().min(1).max(255), - subdomain: z.string().nullable().optional(), + subdomain: z + .string() + .optional() + .transform((val) => val?.toLowerCase()), + isBaseDomain: z.boolean().optional(), + siteId: z.number(), http: z.boolean(), - protocol: z.enum(["tcp", "udp"]), - domainId: z.string(), - stickySession: z.boolean().optional(), + protocol: z.string(), + domainId: z.string() }) .strict() .refine( @@ -47,15 +51,28 @@ const createHttpResourceSchema = z return true; }, { message: "Invalid subdomain" } + ) + .refine( + (data) => { + if (!config.getRawConfig().flags?.allow_base_domain_resources) { + if (data.isBaseDomain) { + return false; + } + } + return true; + }, + { + message: "Base domain resources are not allowed" + } ); const createRawResourceSchema = z .object({ name: z.string().min(1).max(255), + siteId: z.number(), http: z.boolean(), - protocol: z.enum(["tcp", "udp"]), + protocol: z.string(), proxyPort: z.number().int().min(1).max(65535) - // enableProxy: z.boolean().default(true) // always true now }) .strict() .refine( @@ -68,7 +85,7 @@ const createRawResourceSchema = z return true; }, { - message: "Raw resources are not allowed" + message: "Proxy port cannot be set" } ); @@ -76,7 +93,7 @@ export type CreateResourceResponse = Resource; registry.registerPath({ method: "put", - path: "/org/{orgId}/resource", + path: "/org/{orgId}/site/{siteId}/resource", description: "Create a resource.", tags: [OpenAPITags.Org, OpenAPITags.Resource], request: { @@ -84,7 +101,9 @@ registry.registerPath({ body: { content: { "application/json": { - schema: createHttpResourceSchema.or(createRawResourceSchema) + schema: createHttpResourceSchema.or( + createRawResourceSchema + ) } } } @@ -109,7 +128,7 @@ export async function createResource( ); } - const { orgId } = parsedParams.data; + const { siteId, orgId } = parsedParams.data; if (req.user && !req.userRoleIds) { return next( @@ -142,19 +161,15 @@ export async function createResource( const { http } = req.body; if (http) { - return await createHttpResource({ req, res, next }, { orgId }); + return await createHttpResource( + { req, res, next }, + { siteId, orgId } + ); } else { - if ( - !config.getRawConfig().flags?.allow_raw_resources - ) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Raw resources are not allowed" - ) - ); - } - return await createRawResource({ req, res, next }, { orgId }); + return await createRawResource( + { req, res, next }, + { siteId, orgId } + ); } } catch (error) { logger.error(error); @@ -171,11 +186,12 @@ async function createHttpResource( next: NextFunction; }, meta: { + siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { orgId } = meta; + const { siteId, orgId } = meta; const parsedBody = createHttpResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -187,18 +203,34 @@ async function createHttpResource( ); } - const { name, domainId } = parsedBody.data; - const subdomain = parsedBody.data.subdomain; - const stickySession=parsedBody.data.stickySession; + const { name, subdomain, isBaseDomain, http, protocol, domainId } = + parsedBody.data; - // Validate domain and construct full domain - const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain); + const [orgDomain] = await db + .select() + .from(orgDomains) + .where( + and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) + ) + .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); - if (!domainResult.success) { - return next(createHttpError(HttpCode.BAD_REQUEST, domainResult.error)); + if (!orgDomain || !orgDomain.domains) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Domain with ID ${parsedBody.data.domainId} not found` + ) + ); } - const { fullDomain, subdomain: finalSubdomain } = domainResult; + const domain = orgDomain.domains; + + let fullDomain = ""; + if (isBaseDomain) { + fullDomain = domain.baseDomain; + } else { + fullDomain = `${subdomain}.${domain.baseDomain}`; + } logger.debug(`Full domain: ${fullDomain}`); @@ -219,22 +251,20 @@ async function createHttpResource( let resource: Resource | undefined; - const niceId = await getUniqueResourceName(orgId); - await db.transaction(async (trx) => { const newResource = await trx .insert(resources) .values({ - niceId, + siteId, fullDomain, domainId, orgId, name, - subdomain: finalSubdomain, - http: true, - protocol: "tcp", + subdomain, + http, + protocol, ssl: true, - stickySession: stickySession + isBaseDomain }) .returning(); @@ -275,10 +305,6 @@ async function createHttpResource( ); } - /* if (build != "oss") { - await createCertificate(domainId, fullDomain, db); - }*/ - return response(res, { data: resource, success: true, @@ -295,11 +321,12 @@ async function createRawResource( next: NextFunction; }, meta: { + siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { orgId } = meta; + const { siteId, orgId } = meta; const parsedBody = createRawResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -313,21 +340,38 @@ async function createRawResource( const { name, http, protocol, proxyPort } = parsedBody.data; - let resource: Resource | undefined; + // if http is false check to see if there is already a resource with the same port and protocol + const existingResource = await db + .select() + .from(resources) + .where( + and( + eq(resources.protocol, protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); - const niceId = await getUniqueResourceName(orgId); + if (existingResource.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + + let resource: Resource | undefined; await db.transaction(async (trx) => { const newResource = await trx .insert(resources) .values({ - niceId, + siteId, orgId, name, http, protocol, proxyPort - // enableProxy }) .returning(); @@ -348,7 +392,7 @@ async function createRawResource( resourceId: newResource[0].resourceId }); - if (req.user && req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { + if (req.userRoleIds?.indexOf(adminRole[0].roleId) === -1) { // make sure the user can access the resource await trx.insert(userResources).values({ userId: req.user?.userId!, diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index 7cb83d8b..b52713d1 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourceRules, resources } from "@server/db"; +import { resourceRules, resources } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -17,8 +17,8 @@ import { OpenAPITags, registry } from "@server/openApi"; const createResourceRuleSchema = z .object({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]), + action: z.enum(["ACCEPT", "DROP"]), + match: z.enum(["CIDR", "IP", "PATH"]), value: z.string().min(1), priority: z.number().int(), enabled: z.boolean().optional() diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index 3b0e9df4..8b58f688 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; +import { newts, resources, sites, targets } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -71,44 +71,43 @@ export async function deleteResource( ); } - // const [site] = await db - // .select() - // .from(sites) - // .where(eq(sites.siteId, deletedResource.siteId!)) - // .limit(1); - // - // if (!site) { - // return next( - // createHttpError( - // HttpCode.NOT_FOUND, - // `Site with ID ${deletedResource.siteId} not found` - // ) - // ); - // } - // - // if (site.pubKey) { - // if (site.type == "wireguard") { - // await addPeer(site.exitNodeId!, { - // publicKey: site.pubKey, - // allowedIps: await getAllowedIps(site.siteId) - // }); - // } else if (site.type == "newt") { - // // get the newt on the site by querying the newt table for siteId - // const [newt] = await db - // .select() - // .from(newts) - // .where(eq(newts.siteId, site.siteId)) - // .limit(1); - // - // removeTargets( - // newt.newtId, - // targetsToBeRemoved, - // deletedResource.protocol, - // deletedResource.proxyPort - // ); - // } - // } - // + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, deletedResource.siteId!)) + .limit(1); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${deletedResource.siteId} not found` + ) + ); + } + + if (site.pubKey) { + if (site.type == "wireguard") { + await addPeer(site.exitNodeId!, { + publicKey: site.pubKey, + allowedIps: await getAllowedIps(site.siteId) + }); + } else if (site.type == "newt") { + // get the newt on the site by querying the newt table for siteId + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + removeTargets( + newt.newtId, + targetsToBeRemoved, + deletedResource.protocol + ); + } + } + return response(res, { data: null, success: true, diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts index 6b404651..573825b0 100644 --- a/server/routers/resource/deleteResourceRule.ts +++ b/server/routers/resource/deleteResourceRule.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourceRules, resources } from "@server/db"; +import { resourceRules, resources } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/resource/getExchangeToken.ts b/server/routers/resource/getExchangeToken.ts index 605e5ca6..f9579433 100644 --- a/server/routers/resource/getExchangeToken.ts +++ b/server/routers/resource/getExchangeToken.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources } from "@server/db"; +import { resources } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { createResourceSession } from "@server/auth/sessions/resource"; import HttpCode from "@server/types/HttpCode"; @@ -14,7 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; -import { response } from "@server/lib/response"; +import { response } from "@server/lib"; const getExchangeTokenParams = z .object({ diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 0fdcdd0c..ae3c87d3 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -1,75 +1,35 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { Resource, resources, sites } from "@server/db"; -import { eq, and } from "drizzle-orm"; +import { Resource, resources, sites } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; -import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; const getResourceSchema = z .object({ resourceId: z .string() - .optional() - .transform(stoi) - .pipe(z.number().int().positive().optional()) - .optional(), - niceId: z.string().optional(), - orgId: z.string().optional() + .transform(Number) + .pipe(z.number().int().positive()) }) .strict(); -async function query(resourceId?: number, niceId?: string, orgId?: string) { - if (resourceId) { - const [res] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - return res; - } else if (niceId && orgId) { - const [res] = await db - .select() - .from(resources) - .where(and(eq(resources.niceId, niceId), eq(resources.orgId, orgId))) - .limit(1); - return res; - } -} - -export type GetResourceResponse = Omit>>, 'headers'> & { - headers: { name: string; value: string }[] | null; +export type GetResourceResponse = Resource & { + siteName: string; }; -registry.registerPath({ - method: "get", - path: "/org/{orgId}/resource/{niceId}", - description: - "Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", - tags: [OpenAPITags.Org, OpenAPITags.Resource], - request: { - params: z.object({ - orgId: z.string(), - niceId: z.string() - }) - }, - responses: {} -}); - registry.registerPath({ method: "get", path: "/resource/{resourceId}", - description: "Get a resource by resourceId.", + description: "Get a resource.", tags: [OpenAPITags.Resource], request: { - params: z.object({ - resourceId: z.number() - }) + params: getResourceSchema }, responses: {} }); @@ -90,20 +50,31 @@ export async function getResource( ); } - const { resourceId, niceId, orgId } = parsedParams.data; + const { resourceId } = parsedParams.data; - const resource = await query(resourceId, niceId, orgId); + const [resp] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .leftJoin(sites, eq(sites.siteId, resources.siteId)) + .limit(1); + + const resource = resp.resources; + const site = resp.sites; if (!resource) { return next( - createHttpError(HttpCode.NOT_FOUND, "Resource not found") + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) ); } - return response(res, { + return response(res, { data: { ...resource, - headers: resource.headers ? JSON.parse(resource.headers) : resource.headers + siteName: site?.name }, success: true, error: false, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 960dda5e..5f74b637 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -1,12 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; +import { db } from "@server/db"; import { - db, - resourceHeaderAuth, resourcePassword, resourcePincode, resources -} from "@server/db"; +} from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -16,24 +15,22 @@ import logger from "@server/logger"; const getResourceAuthInfoSchema = z .object({ - resourceGuid: z.string() + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) }) .strict(); export type GetResourceAuthInfoResponse = { resourceId: number; - resourceGuid: string; resourceName: string; - niceId: string; password: boolean; pincode: boolean; - headerAuth: boolean; sso: boolean; blockAccess: boolean; url: string; whitelist: boolean; - skipToIdpId: number | null; - orgId: string; }; export async function getResourceAuthInfo( @@ -52,9 +49,7 @@ export async function getResourceAuthInfo( ); } - const { resourceGuid } = parsedParams.data; - - const isGuidInteger = /^\d+$/.test(resourceGuid); + const { resourceId } = parsedParams.data; const [result] = await db .select() @@ -67,42 +62,31 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourceId, resources.resourceId) - ) - .where(eq(resources.resourceGuid, resourceGuid)) + .where(eq(resources.resourceId, resourceId)) .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, - whitelist: resource.emailWhitelistEnabled, - skipToIdpId: resource.skipToIdpId, - orgId: resource.orgId + whitelist: resource.emailWhitelistEnabled }, success: true, error: false, diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 415cb714..321fd331 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourceWhitelist, users } from "@server/db"; // Assuming these are the correct tables +import { resourceWhitelist, users } from "@server/db/schemas"; // Assuming these are the correct tables import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts deleted file mode 100644 index 3d28da6f..00000000 --- a/server/routers/resource/getUserResources.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { and, eq, or, inArray } from "drizzle-orm"; -import { - resources, - userResources, - roleResources, - userOrgs, - resourcePassword, - resourcePincode, - resourceWhitelist -} from "@server/db"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import { response } from "@server/lib/response"; - -export async function getUserResources( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const { orgId } = req.params; - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - // First get the user's role in the organization - const userOrgResult = await db - .select({ - roleId: userOrgs.roleId - }) - .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (userOrgResult.length === 0) { - return next( - createHttpError(HttpCode.FORBIDDEN, "User not in organization") - ); - } - - const userRoleId = userOrgResult[0].roleId; - - // Get resources accessible through direct assignment or role assignment - const directResourcesQuery = db - .select({ resourceId: userResources.resourceId }) - .from(userResources) - .where(eq(userResources.userId, userId)); - - const roleResourcesQuery = db - .select({ resourceId: roleResources.resourceId }) - .from(roleResources) - .where(eq(roleResources.roleId, userRoleId)); - - const [directResources, roleResourceResults] = await Promise.all([ - directResourcesQuery, - roleResourcesQuery - ]); - - // Combine all accessible resource IDs - const accessibleResourceIds = [ - ...directResources.map((r) => r.resourceId), - ...roleResourceResults.map((r) => r.resourceId) - ]; - - if (accessibleResourceIds.length === 0) { - return response(res, { - data: { resources: [] }, - success: true, - error: false, - message: "No resources found", - status: HttpCode.OK - }); - } - - // Get resource details for accessible resources - const resourcesData = await db - .select({ - resourceId: resources.resourceId, - name: resources.name, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - enabled: resources.enabled, - sso: resources.sso, - protocol: resources.protocol, - emailWhitelistEnabled: resources.emailWhitelistEnabled - }) - .from(resources) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId), - eq(resources.enabled, true) - ) - ); - - // Check for password, pincode, and whitelist protection for each resource - const resourcesWithAuth = await Promise.all( - resourcesData.map(async (resource) => { - const [passwordCheck, pincodeCheck, whitelistCheck] = - await Promise.all([ - db - .select() - .from(resourcePassword) - .where( - eq( - resourcePassword.resourceId, - resource.resourceId - ) - ) - .limit(1), - db - .select() - .from(resourcePincode) - .where( - eq( - resourcePincode.resourceId, - resource.resourceId - ) - ) - .limit(1), - db - .select() - .from(resourceWhitelist) - .where( - eq( - resourceWhitelist.resourceId, - resource.resourceId - ) - ) - .limit(1) - ]); - - const hasPassword = passwordCheck.length > 0; - const hasPincode = pincodeCheck.length > 0; - const hasWhitelist = - whitelistCheck.length > 0 || resource.emailWhitelistEnabled; - - return { - resourceId: resource.resourceId, - name: resource.name, - domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, - enabled: resource.enabled, - protected: !!( - resource.sso || - hasPassword || - hasPincode || - hasWhitelist - ), - protocol: resource.protocol, - sso: resource.sso, - password: hasPassword, - pincode: hasPincode, - whitelist: hasWhitelist - }; - }) - ); - - return response(res, { - data: { resources: resourcesWithAuth }, - success: true, - error: false, - message: "User resources retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - console.error("Error fetching user resources:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Internal server error" - ) - ); - } -} - -export type GetUserResourcesResponse = { - success: boolean; - data: { - resources: Array<{ - resourceId: number; - name: string; - domain: string; - enabled: boolean; - protected: boolean; - protocol: string; - }>; - }; -}; diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 60938342..03c9ffbe 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -16,10 +16,9 @@ export * from "./setResourceWhitelist"; export * from "./getResourceWhitelist"; export * from "./authWithWhitelist"; export * from "./authWithAccessToken"; +export * from "./transferResource"; export * from "./getExchangeToken"; export * from "./createResourceRule"; export * from "./deleteResourceRule"; export * from "./listResourceRules"; -export * from "./updateResourceRule"; -export * from "./getUserResources"; -export * from "./setResourceHeaderAuth"; +export * from "./updateResourceRule"; \ No newline at end of file diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index 4676b01e..c173cacb 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleResources, roles } from "@server/db"; +import { roleResources, roles } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts index 727d50ba..f0a0d84c 100644 --- a/server/routers/resource/listResourceRules.ts +++ b/server/routers/resource/listResourceRules.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { resourceRules, resources } from "@server/db"; +import { resourceRules, resources } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq, sql } from "drizzle-orm"; @@ -35,7 +35,7 @@ const listResourceRulesSchema = z.object({ }); function queryResourceRules(resourceId: number) { - const baseQuery = db + let baseQuery = db .select({ ruleId: resourceRules.ruleId, resourceId: resourceRules.resourceId, @@ -117,7 +117,7 @@ export async function listResourceRules( const baseQuery = queryResourceRules(resourceId); - const countQuery = db + let countQuery = db .select({ count: sql`cast(count(*) as integer)` }) .from(resourceRules) .where(eq(resourceRules.resourceId, resourceId)); diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index 0d96ac0d..4699ec8b 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idp, userResources, users } from "@server/db"; // Assuming these are the correct tables +import { idp, userResources, users } from "@server/db/schemas"; // Assuming these are the correct tables import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index eada5e16..49de7aae 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,13 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, resourceHeaderAuth } from "@server/db"; +import { db } from "@server/db"; import { resources, + sites, userResources, roleResources, resourcePassword, resourcePincode -} from "@server/db"; +} from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -16,13 +17,20 @@ import logger from "@server/logger"; import stoi from "@server/lib/stoi"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { warn } from "console"; const listResourcesParamsSchema = z .object({ - orgId: z.string() + siteId: z + .string() + .optional() + .transform(stoi) + .pipe(z.number().int().positive().optional()), + orgId: z.string().optional() }) - .strict(); + .strict() + .refine((data) => !!data.siteId !== !!data.orgId, { + message: "Either siteId or orgId must be provided, but not both" + }); const listResourcesSchema = z.object({ limit: z @@ -40,44 +48,80 @@ const listResourcesSchema = z.object({ .pipe(z.number().int().nonnegative()) }); -function queryResources(accessibleResourceIds: number[], orgId: string) { - return db - .select({ - resourceId: resources.resourceId, - name: resources.name, - ssl: resources.ssl, - fullDomain: resources.fullDomain, - passwordId: resourcePassword.passwordId, - sso: resources.sso, - pincodeId: resourcePincode.pincodeId, - whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, - proxyPort: resources.proxyPort, - enabled: resources.enabled, - domainId: resources.domainId, - niceId: resources.niceId, - headerAuthId: resourceHeaderAuth.headerAuthId - }) - .from(resources) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourceId, resources.resourceId) - ) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId) +function queryResources( + accessibleResourceIds: number[], + siteId?: number, + orgId?: string +) { + if (siteId) { + return db + .select({ + resourceId: resources.resourceId, + name: resources.name, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + siteName: sites.name, + siteId: sites.niceId, + passwordId: resourcePassword.passwordId, + pincodeId: resourcePincode.pincodeId, + sso: resources.sso, + whitelist: resources.emailWhitelistEnabled, + http: resources.http, + protocol: resources.protocol, + proxyPort: resources.proxyPort, + enabled: resources.enabled + }) + .from(resources) + .leftJoin(sites, eq(resources.siteId, sites.siteId)) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) ) - ); + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .where( + and( + inArray(resources.resourceId, accessibleResourceIds), + eq(resources.siteId, siteId) + ) + ); + } else if (orgId) { + return db + .select({ + resourceId: resources.resourceId, + name: resources.name, + ssl: resources.ssl, + fullDomain: resources.fullDomain, + siteName: sites.name, + siteId: sites.niceId, + passwordId: resourcePassword.passwordId, + sso: resources.sso, + pincodeId: resourcePincode.pincodeId, + whitelist: resources.emailWhitelistEnabled, + http: resources.http, + protocol: resources.protocol, + proxyPort: resources.proxyPort, + enabled: resources.enabled + }) + .from(resources) + .leftJoin(sites, eq(resources.siteId, sites.siteId)) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .where( + and( + inArray(resources.resourceId, accessibleResourceIds), + eq(resources.orgId, orgId) + ) + ); + } } export type ListResourcesResponse = { @@ -85,6 +129,20 @@ export type ListResourcesResponse = { pagination: { total: number; limit: number; offset: number }; }; +registry.registerPath({ + method: "get", + path: "/site/{siteId}/resources", + description: "List resources for a site.", + tags: [OpenAPITags.Site, OpenAPITags.Resource], + request: { + params: z.object({ + siteId: z.number() + }), + query: listResourcesSchema + }, + responses: {} +}); + registry.registerPath({ method: "get", path: "/org/{orgId}/resources", @@ -125,11 +183,9 @@ export async function listResources( ) ); } + const { siteId } = parsedParams.data; - const orgId = - parsedParams.data.orgId || - req.userOrg?.orgId || - req.apiKeyOrg?.orgId; + const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId; if (!orgId) { return next( @@ -164,24 +220,21 @@ export async function listResources( ) ); } else { - accessibleResources = await db - .select({ - resourceId: resources.resourceId - }) - .from(resources) - .where(eq(resources.orgId, orgId)); + accessibleResources = await db.select({ + resourceId: resources.resourceId + }).from(resources).where(eq(resources.orgId, orgId)); } const accessibleResourceIds = accessibleResources.map( (resource) => resource.resourceId ); - const countQuery: any = db + let countQuery: any = db .select({ count: count() }) .from(resources) .where(inArray(resources.resourceId, accessibleResourceIds)); - const baseQuery = queryResources(accessibleResourceIds, orgId); + const baseQuery = queryResources(accessibleResourceIds, siteId, orgId); const resourcesList = await baseQuery!.limit(limit).offset(offset); const totalCountResult = await countQuery; diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts deleted file mode 100644 index dc0d417d..00000000 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ /dev/null @@ -1,101 +0,0 @@ -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 5ff485d2..29eb89cb 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -1,13 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourcePassword } from "@server/db"; +import { resourcePassword } from "@server/db/schemas"; import { eq } from "drizzle-orm"; 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/response"; +import { response } from "@server/lib"; 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 83af3c7a..2a1b7c1f 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -1,13 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourcePincode } from "@server/db"; +import { resourcePincode } from "@server/db/schemas"; import { eq } from "drizzle-orm"; 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/response"; +import { response } from "@server/lib"; import stoi from "@server/lib/stoi"; import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index 7ea76d21..0f0b3df2 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, resources } from "@server/db"; -import { apiKeys, roleResources, roles } from "@server/db"; +import { db } from "@server/db"; +import { apiKeys, roleResources, roles } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -74,18 +74,13 @@ export async function setResourceRoles( const { resourceId } = parsedParams.data; - // get the resource - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); + const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId; - if (!resource) { + if (!orgId) { return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Resource not found" + "Organization not found" ) ); } @@ -97,7 +92,7 @@ export async function setResourceRoles( .where( and( eq(roles.name, "Admin"), - eq(roles.orgId, resource.orgId) + eq(roles.orgId, orgId) ) ) .limit(1); diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index 152c0f88..3080ae45 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userResources } from "@server/db"; +import { userResources } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 16c9150b..ceec816c 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, resourceWhitelist } from "@server/db"; +import { resources, resourceWhitelist } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts new file mode 100644 index 00000000..9b21abb2 --- /dev/null +++ b/server/routers/resource/transferResource.ts @@ -0,0 +1,212 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { newts, resources, sites, targets } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { addPeer } from "../gerbil/peers"; +import { addTargets, removeTargets } from "../newt/targets"; +import { getAllowedIps } from "../target/helpers"; +import { OpenAPITags, registry } from "@server/openApi"; + +const transferResourceParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +const transferResourceBodySchema = z + .object({ + siteId: z.number().int().positive() + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/resource/{resourceId}/transfer", + description: + "Transfer a resource to a different site. This will also transfer the targets associated with the resource.", + tags: [OpenAPITags.Resource], + request: { + params: transferResourceParamsSchema, + body: { + content: { + "application/json": { + schema: transferResourceBodySchema + } + } + } + }, + responses: {} +}); + +export async function transferResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = transferResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = transferResourceBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + const { siteId } = parsedBody.data; + + const [oldResource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!oldResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + if (oldResource.siteId === siteId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Resource is already assigned to this site` + ) + ); + } + + const [newSite] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (!newSite) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found` + ) + ); + } + + const [oldSite] = await db + .select() + .from(sites) + .where(eq(sites.siteId, oldResource.siteId)) + .limit(1); + + if (!oldSite) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${oldResource.siteId} not found` + ) + ); + } + + const [updatedResource] = await db + .update(resources) + .set({ siteId }) + .where(eq(resources.resourceId, resourceId)) + .returning(); + + if (!updatedResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + const resourceTargets = await db + .select() + .from(targets) + .where(eq(targets.resourceId, resourceId)); + + if (resourceTargets.length > 0) { + ////// REMOVE THE TARGETS FROM THE OLD SITE ////// + if (oldSite.pubKey) { + if (oldSite.type == "wireguard") { + await addPeer(oldSite.exitNodeId!, { + publicKey: oldSite.pubKey, + allowedIps: await getAllowedIps(oldSite.siteId) + }); + } else if (oldSite.type == "newt") { + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, oldSite.siteId)) + .limit(1); + + removeTargets( + newt.newtId, + resourceTargets, + updatedResource.protocol + ); + } + } + + ////// ADD THE TARGETS TO THE NEW SITE ////// + if (newSite.pubKey) { + if (newSite.type == "wireguard") { + await addPeer(newSite.exitNodeId!, { + publicKey: newSite.pubKey, + allowedIps: await getAllowedIps(newSite.siteId) + }); + } else if (newSite.type == "newt") { + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, newSite.siteId)) + .limit(1); + + addTargets( + newt.newtId, + resourceTargets, + updatedResource.protocol + ); + } + } + } + + return response(res, { + data: updatedResource, + success: true, + error: false, + message: "Resource transferred successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 9aecfaff..a857e103 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, loginPage } from "@server/db"; +import { db } from "@server/db"; import { domains, Org, @@ -8,7 +8,7 @@ import { orgs, Resource, resources -} from "@server/db"; +} from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -20,7 +20,6 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; -import { validateAndConstructDomain } from "@server/lib/domainUtils"; const updateResourceParamsSchema = z .object({ @@ -34,22 +33,20 @@ const updateResourceParamsSchema = z const updateHttpResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - subdomain: subdomainSchema.nullable().optional(), + subdomain: subdomainSchema + .optional() + .transform((val) => val?.toLowerCase()), ssl: z.boolean().optional(), sso: z.boolean().optional(), blockAccess: z.boolean().optional(), emailWhitelistEnabled: z.boolean().optional(), + isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), domainId: z.string().optional(), enabled: z.boolean().optional(), stickySession: z.boolean().optional(), 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() + setHostHeader: z.string().nullable().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -64,6 +61,19 @@ const updateHttpResourceBodySchema = z }, { message: "Invalid subdomain" } ) + .refine( + (data) => { + if (!config.getRawConfig().flags?.allow_base_domain_resources) { + if (data.isBaseDomain) { + return false; + } + } + return true; + }, + { + message: "Base domain resources are not allowed" + } + ) .refine( (data) => { if (data.tlsServerName) { @@ -97,7 +107,6 @@ const updateRawResourceBodySchema = z proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), enabled: z.boolean().optional() - // enableProxy: z.boolean().optional() // always true now }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -233,64 +242,86 @@ async function updateHttpResource( const updateData = parsedBody.data; if (updateData.domainId) { - const domainId = updateData.domainId; + const [existingDomain] = await db + .select() + .from(orgDomains) + .where( + and( + eq(orgDomains.orgId, org.orgId), + eq(orgDomains.domainId, updateData.domainId) + ) + ) + .leftJoin(domains, eq(orgDomains.domainId, domains.domainId)); - // Validate domain and construct full domain - const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain); - - if (!domainResult.success) { + if (!existingDomain) { return next( - createHttpError(HttpCode.BAD_REQUEST, domainResult.error) + createHttpError(HttpCode.NOT_FOUND, `Domain not found`) ); } - - const { fullDomain, subdomain: finalSubdomain } = domainResult; - - logger.debug(`Full domain: ${fullDomain}`); - - if (fullDomain) { - const [existingDomain] = await db - .select() - .from(resources) - .where(eq(resources.fullDomain, fullDomain)); - - if ( - existingDomain && - existingDomain.resourceId !== resource.resourceId - ) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Resource with that domain already exists" - ) - ); - } - } - - // update the full domain if it has changed - if (fullDomain && fullDomain !== resource.fullDomain) { - await db - .update(resources) - .set({ fullDomain }) - .where(eq(resources.resourceId, resource.resourceId)); - } - - // Update the subdomain in the update data - updateData.subdomain = finalSubdomain; - - /* if (build != "oss") { - await createCertificate(domainId, fullDomain, db); - }*/ } - let headers = null; - if (updateData.headers) { - headers = JSON.stringify(updateData.headers); + const domainId = updateData.domainId || resource.domainId!; + const subdomain = updateData.subdomain || resource.subdomain; + + const [domain] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)); + + const isBaseDomain = + updateData.isBaseDomain !== undefined + ? updateData.isBaseDomain + : resource.isBaseDomain; + + let fullDomain: string | null = null; + if (isBaseDomain) { + fullDomain = domain.baseDomain; + } else if (subdomain && domain) { + fullDomain = `${subdomain}.${domain.baseDomain}`; } + if (fullDomain) { + const [existingDomain] = await db + .select() + .from(resources) + .where(eq(resources.fullDomain, fullDomain)); + + if ( + existingDomain && + existingDomain.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that domain already exists" + ) + ); + } + } + + const updatePayload = { + ...updateData, + fullDomain + }; + const updatedResource = await db .update(resources) - .set({ ...updateData, headers }) + .set({ + name: updatePayload.name, + subdomain: updatePayload.subdomain, + ssl: updatePayload.ssl, + sso: updatePayload.sso, + blockAccess: updatePayload.blockAccess, + emailWhitelistEnabled: updatePayload.emailWhitelistEnabled, + isBaseDomain: updatePayload.isBaseDomain, + applyRules: updatePayload.applyRules, + domainId: updatePayload.domainId, + enabled: updatePayload.enabled, + stickySession: updatePayload.stickySession, + tlsServerName: updatePayload.tlsServerName || null, + setHostHeader: updatePayload.setHostHeader || null, + fullDomain: updatePayload.fullDomain + }) .where(eq(resources.resourceId, resource.resourceId)) .returning(); @@ -338,6 +369,31 @@ async function updateRawResource( const updateData = parsedBody.data; + if (updateData.proxyPort) { + const proxyPort = updateData.proxyPort; + const existingResource = await db + .select() + .from(resources) + .where( + and( + eq(resources.protocol, resource.protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); + + if ( + existingResource.length > 0 && + existingResource[0].resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + } + const updatedResource = await db .update(resources) .set(updateData) diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index 06061da9..9a953500 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resourceRules, resources } from "@server/db"; +import { resourceRules, resources } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -29,8 +29,8 @@ const updateResourceRuleParamsSchema = z // Define Zod schema for request body validation const updateResourceRuleSchema = z .object({ - action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), - match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]).optional(), + action: z.enum(["ACCEPT", "DROP"]).optional(), + match: z.enum(["CIDR", "IP", "PATH"]).optional(), value: z.string().min(1).optional(), priority: z.number().int(), enabled: z.boolean().optional() diff --git a/server/routers/role/addRoleAction.ts b/server/routers/role/addRoleAction.ts index 62ab87b5..9f364a55 100644 --- a/server/routers/role/addRoleAction.ts +++ b/server/routers/role/addRoleAction.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleActions, roles } from "@server/db"; +import { roleActions, roles } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index d268eed4..0db6ac4e 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, roleResources, roleSites } from "@server/db"; +import { resources, roleResources, roleSites } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -60,18 +60,18 @@ export async function addRoleSite( }) .returning(); - // const siteResources = await db - // .select() - // .from(resources) - // .where(eq(resources.siteId, siteId)); - // - // for (const resource of siteResources) { - // await trx.insert(roleResources).values({ - // roleId, - // resourceId: resource.resourceId - // }); - // } - // + const siteResources = await db + .select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await trx.insert(roleResources).values({ + roleId, + resourceId: resource.resourceId + }); + } + return response(res, { data: newRoleSite[0], success: true, diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index f66c95e2..3bc363f6 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs, Role, roleActions, roles } from "@server/db"; +import { orgs, Role, roleActions, roles } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index 6806386e..a89428d5 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles, userOrgs } from "@server/db"; +import { roles, userOrgs } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts index 66dbb68f..20f93bf4 100644 --- a/server/routers/role/getRole.ts +++ b/server/routers/role/getRole.ts @@ -1,14 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles } from "@server/db"; +import { roles } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; const getRoleSchema = z .object({ @@ -16,17 +15,6 @@ const getRoleSchema = z }) .strict(); -registry.registerPath({ - method: "get", - path: "/role/{roleId}", - description: "Get a role.", - tags: [OpenAPITags.Role], - request: { - params: getRoleSchema - }, - responses: {} -}); - export async function getRole( req: Request, res: Response, diff --git a/server/routers/role/index.ts b/server/routers/role/index.ts index bbbe4ba8..0194c1f0 100644 --- a/server/routers/role/index.ts +++ b/server/routers/role/index.ts @@ -1,5 +1,6 @@ export * from "./addRoleAction"; export * from "../resource/setResourceRoles"; +export * from "./addRoleSite"; export * from "./createRole"; export * from "./deleteRole"; export * from "./getRole"; @@ -10,4 +11,5 @@ export * from "./listRoles"; export * from "./listRoleSites"; export * from "./removeRoleAction"; export * from "./removeRoleResource"; -export * from "./updateRole"; +export * from "./removeRoleSite"; +export * from "./updateRole"; \ No newline at end of file diff --git a/server/routers/role/listRoleActions.ts b/server/routers/role/listRoleActions.ts index cdf1391b..d4637092 100644 --- a/server/routers/role/listRoleActions.ts +++ b/server/routers/role/listRoleActions.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleActions, actions } from "@server/db"; +import { roleActions, actions } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/listRoleResources.ts b/server/routers/role/listRoleResources.ts index ba254f1d..7239f6f7 100644 --- a/server/routers/role/listRoleResources.ts +++ b/server/routers/role/listRoleResources.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleResources, resources } from "@server/db"; +import { roleResources, resources } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/listRoleSites.ts b/server/routers/role/listRoleSites.ts index 72f49e3a..f6594545 100644 --- a/server/routers/role/listRoleSites.ts +++ b/server/routers/role/listRoleSites.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleSites, sites } from "@server/db"; +import { roleSites, sites } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index 56ae8a3a..73834b53 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles, orgs } from "@server/db"; +import { roles, orgs } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -60,7 +60,7 @@ export type ListRolesResponse = { registry.registerPath({ method: "get", - path: "/org/{orgId}/roles", + path: "/orgs/{orgId}/roles", description: "List roles.", tags: [OpenAPITags.Org, OpenAPITags.Role], request: { @@ -100,7 +100,7 @@ export async function listRoles( const { orgId } = parsedParams.data; - const countQuery: any = db + let countQuery: any = db .select({ count: sql`cast(count(*) as integer)` }) .from(roles) .where(eq(roles.orgId, orgId)); diff --git a/server/routers/role/removeRoleAction.ts b/server/routers/role/removeRoleAction.ts index e643ae04..72d9be53 100644 --- a/server/routers/role/removeRoleAction.ts +++ b/server/routers/role/removeRoleAction.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleActions } from "@server/db"; +import { roleActions } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/removeRoleResource.ts b/server/routers/role/removeRoleResource.ts index 4068b0bd..ca068e05 100644 --- a/server/routers/role/removeRoleResource.ts +++ b/server/routers/role/removeRoleResource.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleResources } from "@server/db"; +import { roleResources } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index 2670272d..a99adf5c 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, roleResources, roleSites } from "@server/db"; +import { resources, roleResources, roleSites } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -71,22 +71,22 @@ export async function removeRoleSite( ); } - // const siteResources = await db - // .select() - // .from(resources) - // .where(eq(resources.siteId, siteId)); - // - // for (const resource of siteResources) { - // await trx - // .delete(roleResources) - // .where( - // and( - // eq(roleResources.roleId, roleId), - // eq(roleResources.resourceId, resource.resourceId) - // ) - // ) - // .returning(); - // } + const siteResources = await db + .select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await trx + .delete(roleResources) + .where( + and( + eq(roleResources.roleId, roleId), + eq(roleResources.resourceId, resource.resourceId) + ) + ) + .returning(); + } }); return response(res, { diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 793be6eb..bf029eb1 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles } from "@server/db"; +import { roles } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index f1a6428d..a4444b83 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,22 +1,19 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, db, exitNodes } from "@server/db"; -import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db"; +import { db } from "@server/db"; +import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { eq, and } from "drizzle-orm"; -import { getUniqueSiteName } from "../../db/names"; +import { getUniqueSiteName } from "@server/db/names"; import { addPeer } from "../gerbil/peers"; import { fromError } from "zod-validation-error"; -import { newts } from "@server/db"; +import { newts } from "@server/db/schemas"; import moment from "moment"; 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 { verifyExitNodeOrgAccess } from "@server/lib/exitNodes"; const createSiteParamsSchema = z .object({ @@ -38,18 +35,9 @@ const createSiteSchema = z subnet: z.string().optional(), newtId: z.string().optional(), secret: z.string().optional(), - 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; -// }); export type CreateSiteBody = z.infer; @@ -96,8 +84,7 @@ export async function createSite( pubKey, subnet, newtId, - secret, - address + secret } = parsedBody.data; const parsedParams = createSiteParamsSchema.safeParse(req.params); @@ -129,84 +116,12 @@ export async function createSite( ); } - let updatedAddress = null; - if (address) { - if (!org.subnet) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Organization with ID ${orgId} has no subnet defined` - ) - ); - } - - if (!isValidIP(address)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Invalid address format. Please provide a valid IP notation." - ) - ); - } - - if (!isIpInCidr(address, org.subnet)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "IP is not in the CIDR range of the subnet." - ) - ); - } - - updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org - - // make sure the subnet is unique - const addressExistsSites = await db - .select() - .from(sites) - .where( - and( - eq(sites.address, updatedAddress), - eq(sites.orgId, orgId) - ) - ) - .limit(1); - - if (addressExistsSites.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - `Subnet ${updatedAddress} already exists in sites` - ) - ); - } - - const addressExistsClients = await db - .select() - .from(clients) - .where( - and( - eq(clients.subnet, updatedAddress), - eq(clients.orgId, orgId) - ) - ) - .limit(1); - if (addressExistsClients.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - `Subnet ${updatedAddress} already exists in clients` - ) - ); - } - } - const niceId = await getUniqueSiteName(orgId); await db.transaction(async (trx) => { let newSite: Site; - if ((type == "wireguard" || type == "newt") && exitNodeId) { + if (exitNodeId) { // we are creating a site with an exit node (tunneled) if (!subnet) { return next( @@ -217,32 +132,6 @@ export async function createSite( ); } - const { exitNode, hasAccess } = - await verifyExitNodeOrgAccess( - exitNodeId, - orgId - ); - - if (!exitNode) { - logger.warn("Exit node not found"); - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Exit node not found" - ) - ); - } - - if (!hasAccess) { - logger.warn("Not authorized to use this exit node"); - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Not authorized to use this exit node" - ) - ); - } - [newSite] = await trx .insert(sites) .values({ @@ -250,10 +139,8 @@ export async function createSite( exitNodeId, name, niceId, - address: updatedAddress || null, subnet, type, - dockerSocketEnabled: type == "newt", ...(pubKey && type == "wireguard" && { pubKey }) }) .returning(); @@ -263,15 +150,11 @@ export async function createSite( [newSite] = await trx .insert(sites) .values({ - exitNodeId: exitNodeId, orgId, name, niceId, - address: updatedAddress || null, type, - dockerSocketEnabled: false, - online: true, - subnet: "0.0.0.0/32" + subnet: "0.0.0.0/0" }) .returning(); } diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 5dc68f14..667ab5c8 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { newts, newtSessions, sites } from "@server/db"; +import { newts, newtSessions, sites } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -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 "@server/routers/ws"; +import { sendToClient } from "../ws"; import { OpenAPITags, registry } from "@server/openApi"; const deleteSiteSchema = z @@ -62,8 +62,6 @@ export async function deleteSite( ); } - let deletedNewtId: string | null = null; - await db.transaction(async (trx) => { if (site.pubKey) { if (site.type == "wireguard") { @@ -75,7 +73,11 @@ export async function deleteSite( .where(eq(newts.siteId, siteId)) .returning(); if (deletedNewt) { - deletedNewtId = deletedNewt.newtId; + const payload = { + type: `newt/terminate`, + data: {} + }; + sendToClient(deletedNewt.newtId, payload); // delete all of the sessions for the newt await trx @@ -88,18 +90,6 @@ export async function deleteSite( await trx.delete(sites).where(eq(sites.siteId, siteId)); }); - // Send termination message outside of transaction to prevent blocking - if (deletedNewtId) { - const payload = { - type: `newt/terminate`, - data: {} - }; - // Don't await this to prevent blocking the response - sendToClient(deletedNewtId, payload).catch(error => { - logger.error("Failed to send termination message to newt:", error); - }); - } - return response(res, { data: null, success: true, diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index a9785fa4..4baa85cc 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { sites } from "@server/db"; +import { sites } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/site/index.ts b/server/routers/site/index.ts index 3edf67c1..63505991 100644 --- a/server/routers/site/index.ts +++ b/server/routers/site/index.ts @@ -3,6 +3,5 @@ export * from "./createSite"; export * from "./deleteSite"; export * from "./updateSite"; export * from "./listSites"; -export * from "./listSiteRoles"; -export * from "./pickSiteDefaults"; -export * from "./socketIntegration"; +export * from "./listSiteRoles" +export * from "./pickSiteDefaults"; \ No newline at end of file diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts index 009e0907..13c8dd41 100644 --- a/server/routers/site/listSiteRoles.ts +++ b/server/routers/site/listSiteRoles.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roleSites, roles } from "@server/db"; +import { roleSites, roles } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index e1bb88f6..8dde88fe 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,5 +1,5 @@ -import { db, exitNodes, newts } from "@server/db"; -import { orgs, roleSites, sites, userSites } from "@server/db"; +import { db } from "@server/db"; +import { orgs, roleSites, sites, userSites } from "@server/db/schemas"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -9,66 +9,6 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import NodeCache from "node-cache"; -import semver from "semver"; - -const newtVersionCache = new NodeCache({ stdTTL: 3600 }); // 1 hours in seconds - -async function getLatestNewtVersion(): Promise { - try { - const cachedVersion = newtVersionCache.get("latestNewtVersion"); - if (cachedVersion) { - return cachedVersion; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 1500); // Reduced timeout to 1.5 seconds - - const response = await fetch( - "https://api.github.com/repos/fosrl/newt/tags", - { - signal: controller.signal - } - ); - - clearTimeout(timeoutId); - - if (!response.ok) { - logger.warn( - `Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}` - ); - return null; - } - - const tags = await response.json(); - if (!Array.isArray(tags) || tags.length === 0) { - logger.warn("No tags found for Newt repository"); - return null; - } - - const latestVersion = tags[0].name; - - newtVersionCache.set("latestNewtVersion", latestVersion); - - return latestVersion; - } catch (error: any) { - if (error.name === "AbortError") { - logger.warn( - "Request to fetch latest Newt version timed out (1.5s)" - ); - } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { - logger.warn( - "Connection timeout while fetching latest Newt version" - ); - } else { - logger.warn( - "Error fetching latest Newt version:", - error.message || error - ); - } - return null; - } -} const listSitesParamsSchema = z .object({ @@ -103,17 +43,10 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { megabytesOut: sites.megabytesOut, orgName: orgs.name, type: sites.type, - online: sites.online, - address: sites.address, - newtVersion: newts.version, - exitNodeId: sites.exitNodeId, - exitNodeName: exitNodes.name, - exitNodeEndpoint: exitNodes.endpoint + online: sites.online }) .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), @@ -122,12 +55,8 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { ); } -type SiteWithUpdateAvailable = Awaited>[0] & { - newtUpdateAvailable?: boolean; -}; - export type ListSitesResponse = { - sites: SiteWithUpdateAvailable[]; + sites: Awaited>; pagination: { total: number; limit: number; offset: number }; }; @@ -204,7 +133,7 @@ export async function listSites( const accessibleSiteIds = accessibleSites.map((site) => site.siteId); const baseQuery = querySites(orgId, accessibleSiteIds); - const countQuery = db + let countQuery = db .select({ count: count() }) .from(sites) .where( @@ -218,51 +147,9 @@ export async function listSites( const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; - // Get latest version asynchronously without blocking the response - const latestNewtVersionPromise = getLatestNewtVersion(); - - const sitesWithUpdates: SiteWithUpdateAvailable[] = sitesList.map( - (site) => { - const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; - // Initially set to false, will be updated if version check succeeds - siteWithUpdate.newtUpdateAvailable = false; - return siteWithUpdate; - } - ); - - // Try to get the latest version, but don't block if it fails - try { - const latestNewtVersion = await latestNewtVersionPromise; - - if (latestNewtVersion) { - sitesWithUpdates.forEach((site) => { - if ( - site.type === "newt" && - site.newtVersion && - latestNewtVersion - ) { - try { - site.newtUpdateAvailable = semver.lt( - site.newtVersion, - latestNewtVersion - ); - } catch (error) { - site.newtUpdateAvailable = false; - } - } - }); - } - } catch (error) { - // Log the error but don't let it block the response - logger.warn( - "Failed to check for Newt updates, continuing without update info:", - error - ); - } - return response(res, { data: { - sites: sitesWithUpdates, + sites: sitesList, pagination: { total: totalCount, limit, diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 46d3c53b..92b93e3c 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -1,21 +1,16 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; -import { exitNodes, sites } from "@server/db"; +import { exitNodes, sites } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { - findNextAvailableCidr, - getNextAvailableClientSubnet -} from "@server/lib/ip"; +import { findNextAvailableCidr } from "@server/lib/ip"; import { generateId } from "@server/auth/sessions/app"; import config from "@server/lib/config"; import { OpenAPITags, registry } from "@server/openApi"; -import { fromError } from "zod-validation-error"; import { z } from "zod"; -import { listExitNodes } from "@server/lib/exitNodes"; export type PickSiteDefaultsResponse = { exitNodeId: number; @@ -24,10 +19,9 @@ export type PickSiteDefaultsResponse = { name: string; listenPort: number; endpoint: string; - subnet: string; // TODO: make optional? + subnet: string; newtId: string; newtSecret: string; - clientAddress?: string; }; registry.registerPath({ @@ -44,42 +38,25 @@ registry.registerPath({ responses: {} }); -const pickSiteDefaultsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - export async function pickSiteDefaults( req: Request, res: Response, next: NextFunction ): Promise { try { - const parsedParams = pickSiteDefaultsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; // TODO: more intelligent way to pick the exit node - const exitNodesList = await listExitNodes(orgId); - - const randomExitNode = - exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; - - if (!randomExitNode) { + // make sure there is an exit node by counting the exit nodes table + const nodes = await db.select().from(exitNodes); + if (nodes.length === 0) { return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "No available exit node") + createHttpError(HttpCode.NOT_FOUND, "No exit nodes available") ); } + // get the first exit node + const exitNode = nodes[0]; + // TODO: this probably can be optimized... // list all of the sites on that exit node const sitesQuery = await db @@ -87,16 +64,13 @@ export async function pickSiteDefaults( subnet: sites.subnet }) .from(sites) - .where(eq(sites.exitNodeId, randomExitNode.exitNodeId)); + .where(eq(sites.exitNodeId, exitNode.exitNodeId)); // TODO: we need to lock this subnet for some time so someone else does not take it - const subnets = sitesQuery - .map((site) => site.subnet) - .filter((subnet) => subnet && /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)) - .filter((subnet) => subnet !== null); + let subnets = sitesQuery.map((site) => site.subnet); // exclude the exit node address by replacing after the / with a site block size subnets.push( - randomExitNode.address.replace( + exitNode.address.replace( /\/\d+$/, `/${config.getRawConfig().gerbil.site_block_size}` ) @@ -104,7 +78,7 @@ export async function pickSiteDefaults( const newSubnet = findNextAvailableCidr( subnets, config.getRawConfig().gerbil.site_block_size, - randomExitNode.address + exitNode.address ); if (!newSubnet) { return next( @@ -115,38 +89,24 @@ export async function pickSiteDefaults( ); } - const newClientAddress = await getNextAvailableClientSubnet(orgId); - if (!newClientAddress) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "No available subnet found" - ) - ); - } - - const clientAddress = newClientAddress.split("/")[0]; - const newtId = generateId(15); const secret = generateId(48); return response(res, { data: { - exitNodeId: randomExitNode.exitNodeId, - address: randomExitNode.address, - publicKey: randomExitNode.publicKey, - name: randomExitNode.name, - listenPort: randomExitNode.listenPort, - endpoint: randomExitNode.endpoint, - // subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet + exitNodeId: exitNode.exitNodeId, + address: exitNode.address, + publicKey: exitNode.publicKey, + name: exitNode.name, + listenPort: exitNode.listenPort, + endpoint: exitNode.endpoint, subnet: newSubnet, - clientAddress: clientAddress, newtId, newtSecret: secret }, success: true, error: false, - message: "Site defaults chosen successfully", + message: "Organization retrieved successfully", status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts deleted file mode 100644 index 20395641..00000000 --- a/server/routers/site/socketIntegration.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { db } from "@server/db"; -import { newts, sites } from "@server/db"; -import logger from "@server/logger"; -import HttpCode from "@server/types/HttpCode"; -import response from "@server/lib/response"; -import { eq } from "drizzle-orm"; -import { NextFunction, Request, Response } from "express"; -import createHttpError from "http-errors"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import stoi from "@server/lib/stoi"; -import { sendToClient } from "@server/routers/ws"; -import { - fetchContainers, - dockerSocketCache, - dockerSocket -} from "../newt/dockerSocket"; - -export interface ContainerNetwork { - networkId: string; - endpointId: string; - gateway?: string; - ipAddress?: string; - ipPrefixLen?: number; - macAddress?: string; - aliases?: string[]; - dnsNames?: string[]; -} - -export interface ContainerPort { - privatePort: number; - publicPort?: number; - type: "tcp" | "udp"; - ip?: string; -} - -export interface Container { - id: string; - name: string; - image: string; - state: "running" | "exited" | "paused" | "created"; - status: string; - ports?: ContainerPort[]; - labels: Record; - created: number; - networks: Record; -} - -const siteIdParamsSchema = z - .object({ - siteId: z.string().transform(stoi).pipe(z.number().int().positive()) - }) - .strict(); - -const DockerStatusSchema = z - .object({ - isAvailable: z.boolean(), - socketPath: z.string().optional() - }) - .strict(); - -function validateSiteIdParams(params: any) { - const parsedParams = siteIdParamsSchema.safeParse(params); - if (!parsedParams.success) { - throw createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error) - ); - } - return parsedParams.data; -} - -async function getSiteAndValidateNewt(siteId: number) { - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!site) { - throw createHttpError(HttpCode.NOT_FOUND, "Site not found"); - } - - if (site.type !== "newt") { - throw createHttpError( - HttpCode.BAD_REQUEST, - "This endpoint is only for Newt sites" - ); - } - - return site; -} - -async function getNewtBySiteId(siteId: number) { - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, siteId)) - .limit(1); - - if (!newt) { - throw createHttpError(HttpCode.NOT_FOUND, "Newt not found for site"); - } - - return newt; -} - -async function getSiteAndNewt(siteId: number) { - const site = await getSiteAndValidateNewt(siteId); - const newt = await getNewtBySiteId(siteId); - return { site, newt }; -} - -function asyncHandler( - operation: (siteId: number) => Promise, - successMessage: string -) { - return async ( - req: Request, - res: Response, - next: NextFunction - ): Promise => { - try { - const { siteId } = validateSiteIdParams(req.params); - const result = await operation(siteId); - - return response(res, { - data: result, - success: true, - error: false, - message: successMessage, - status: HttpCode.OK - }); - } catch (error) { - if (createHttpError.isHttpError(error)) { - return next(error); - } - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred" - ) - ); - } - }; -} - -// Core business logic functions -async function triggerFetch(siteId: number) { - const { newt } = await getSiteAndNewt(siteId); - - logger.info( - `Triggering fetch containers for site ${siteId} with Newt ${newt.newtId}` - ); - fetchContainers(newt.newtId); - - // clear the cache for this Newt ID so that the site has to keep asking for the containers - // this is to ensure that the site always gets the latest data - dockerSocketCache.del(`${newt.newtId}:dockerContainers`); - - return { siteId, newtId: newt.newtId }; -} - -async function queryContainers(siteId: number) { - const { newt } = await getSiteAndNewt(siteId); - - const result = dockerSocketCache.get( - `${newt.newtId}:dockerContainers` - ) as Container[]; - if (!result) { - throw createHttpError( - HttpCode.TOO_EARLY, - "Nothing found yet. Perhaps the fetch is still in progress? Wait a bit and try again." - ); - } - - return result; -} - -async function isDockerAvailable(siteId: number): Promise { - const { newt } = await getSiteAndNewt(siteId); - - const key = `${newt.newtId}:isAvailable`; - const isAvailable = dockerSocketCache.get(key); - - return !!isAvailable; -} - -async function getDockerStatus( - siteId: number -): Promise> { - const { newt } = await getSiteAndNewt(siteId); - - const keys = ["isAvailable", "socketPath"]; - const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`); - - const result = { - isAvailable: dockerSocketCache.get(mappedKeys[0]) as boolean, - socketPath: dockerSocketCache.get(mappedKeys[1]) as string | undefined - }; - - return result; -} - -async function checkSocket( - siteId: number -): Promise<{ siteId: number; newtId: string }> { - const { newt } = await getSiteAndNewt(siteId); - - logger.info( - `Checking Docker socket for site ${siteId} with Newt ${newt.newtId}` - ); - - // Trigger the Docker socket check - dockerSocket(newt.newtId); - return { siteId, newtId: newt.newtId }; -} - -// Export types -export type GetDockerStatusResponse = NonNullable< - Awaited> ->; - -export type ListContainersResponse = Awaited< - ReturnType ->; - -export type TriggerFetchResponse = Awaited>; - -// Route handlers -export const triggerFetchContainers = asyncHandler( - triggerFetch, - "Fetch containers triggered successfully" -); - -export const listContainers = asyncHandler( - queryContainers, - "Containers retrieved successfully" -); - -export const dockerOnline = asyncHandler(async (siteId: number) => { - const isAvailable = await isDockerAvailable(siteId); - return { isAvailable }; -}, "Docker availability checked successfully"); - -export const dockerStatus = asyncHandler( - getDockerStatus, - "Docker status retrieved successfully" -); - -export async function checkDockerSocket( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const { siteId } = validateSiteIdParams(req.params); - const result = await checkSocket(siteId); - - // Notify the Newt client about the Docker socket check - sendToClient(result.newtId, { - type: "newt/socket/check", - data: {} - }); - - return response(res, { - data: result, - success: true, - error: false, - message: "Docker socket checked successfully", - status: HttpCode.OK - }); - } catch (error) { - if (createHttpError.isHttpError(error)) { - return next(error); - } - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index e3724f36..43cd848a 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { sites } from "@server/db"; +import { sites } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -9,7 +9,6 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { isValidCIDR } from "@server/lib/validators"; const updateSiteParamsSchema = z .object({ @@ -20,10 +19,6 @@ const updateSiteParamsSchema = z const updateSiteBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - dockerSocketEnabled: z.boolean().optional(), - remoteSubnets: z - .string() - .optional() // subdomain: z // .string() // .min(1) @@ -89,21 +84,6 @@ export async function updateSite( const { siteId } = parsedParams.data; const updateData = parsedBody.data; - // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs - if (updateData.remoteSubnets) { - const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim()); - for (const subnet of subnets) { - if (!isValidCIDR(subnet)) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Invalid CIDR format: ${subnet}` - ) - ); - } - } - } - const updatedSite = await db .update(sites) .set(updateData) diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts deleted file mode 100644 index ca223b04..00000000 --- a/server/routers/siteResource/createSiteResource.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, newts } from "@server/db"; -import { siteResources, sites, orgs, SiteResource } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; -import { addTargets } from "../client/targets"; -import { getUniqueSiteResourceName } from "@server/db/names"; - -const createSiteResourceParamsSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), - orgId: z.string() - }) - .strict(); - -const createSiteResourceSchema = z - .object({ - name: z.string().min(1).max(255), - protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().positive(), - destinationPort: z.number().int().positive(), - destinationIp: z.string(), - enabled: z.boolean().default(true) - }) - .strict(); - -export type CreateSiteResourceBody = z.infer; -export type CreateSiteResourceResponse = SiteResource; - -registry.registerPath({ - method: "put", - path: "/org/{orgId}/site/{siteId}/resource", - description: "Create a new site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: createSiteResourceParamsSchema, - body: { - content: { - "application/json": { - schema: createSiteResourceSchema - } - } - } - }, - responses: {} -}); - -export async function createSiteResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = createSiteResourceParamsSchema.safeParse( - req.params - ); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = createSiteResourceSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { siteId, orgId } = parsedParams.data; - const { - name, - protocol, - proxyPort, - destinationPort, - destinationIp, - enabled - } = parsedBody.data; - - // Verify the site exists and belongs to the org - const [site] = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } - - // check if resource with same protocol and proxy port already exists - const [existingResource] = await db - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId), - eq(siteResources.protocol, protocol), - eq(siteResources.proxyPort, proxyPort) - ) - ) - .limit(1); - if (existingResource && existingResource.siteResourceId) { - return next( - createHttpError( - HttpCode.CONFLICT, - "A resource with the same protocol and proxy port already exists" - ) - ); - } - - const niceId = await getUniqueSiteResourceName(orgId); - - // Create the site resource - const [newSiteResource] = await db - .insert(siteResources) - .values({ - siteId, - niceId, - orgId, - name, - protocol, - proxyPort, - destinationPort, - destinationIp, - enabled - }) - .returning(); - - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); - } - - await addTargets(newt.newtId, destinationIp, destinationPort, protocol, proxyPort); - - logger.info( - `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` - ); - - return response(res, { - data: newSiteResource, - success: true, - error: false, - message: "Site resource created successfully", - status: HttpCode.CREATED - }); - } catch (error) { - logger.error("Error creating site resource:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to create site resource" - ) - ); - } -} diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts deleted file mode 100644 index 347d4b53..00000000 --- a/server/routers/siteResource/deleteSiteResource.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, newts, sites } from "@server/db"; -import { siteResources } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; -import { removeTargets } from "../client/targets"; - -const deleteSiteResourceParamsSchema = z - .object({ - siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), - orgId: z.string() - }) - .strict(); - -export type DeleteSiteResourceResponse = { - message: string; -}; - -registry.registerPath({ - method: "delete", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", - description: "Delete a site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: deleteSiteResourceParamsSchema - }, - responses: {} -}); - -export async function deleteSiteResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = deleteSiteResourceParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { siteResourceId, siteId, orgId } = parsedParams.data; - - const [site] = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } - - // Check if site resource exists - const [existingSiteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); - - if (!existingSiteResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) - ); - } - - // Delete the site resource - await db - .delete(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )); - - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); - } - - await removeTargets( - newt.newtId, - existingSiteResource.destinationIp, - existingSiteResource.destinationPort, - existingSiteResource.protocol, - existingSiteResource.proxyPort - ); - - logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); - - return response(res, { - data: { message: "Site resource deleted successfully" }, - success: true, - error: false, - message: "Site resource deleted successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error("Error deleting site resource:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to delete site resource")); - } -} diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts deleted file mode 100644 index 09c01eb0..00000000 --- a/server/routers/siteResource/getSiteResource.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { siteResources, SiteResource } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; - -const getSiteResourceParamsSchema = z - .object({ - siteResourceId: z - .string() - .optional() - .transform((val) => val ? Number(val) : undefined) - .pipe(z.number().int().positive().optional()) - .optional(), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), - niceId: z.string().optional(), - orgId: z.string() - }) - .strict(); - -async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) { - if (siteResourceId && siteId && orgId) { - const [siteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); - return siteResource; - } else if (niceId && siteId && orgId) { - const [siteResource] = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.niceId, niceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(1); - return siteResource; - } -} - -export type GetSiteResourceResponse = NonNullable>>; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", - description: "Get a specific site resource by siteResourceId.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: z.object({ - siteResourceId: z.number(), - siteId: z.number(), - orgId: z.string() - }) - }, - responses: {} -}); - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/site/{siteId}/resource/nice/{niceId}", - description: "Get a specific site resource by niceId.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: z.object({ - niceId: z.string(), - siteId: z.number(), - orgId: z.string() - }) - }, - responses: {} -}); - -export async function getSiteResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = getSiteResourceParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { siteResourceId, siteId, niceId, orgId } = parsedParams.data; - - // Get the site resource - const siteResource = await query(siteResourceId, siteId, niceId, orgId); - - if (!siteResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site resource not found" - ) - ); - } - - return response(res, { - data: siteResource, - success: true, - error: false, - message: "Site resource retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error("Error getting site resource:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to get site resource")); - } -} diff --git a/server/routers/siteResource/index.ts b/server/routers/siteResource/index.ts deleted file mode 100644 index 2c3e2526..00000000 --- a/server/routers/siteResource/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./createSiteResource"; -export * from "./deleteSiteResource"; -export * from "./getSiteResource"; -export * from "./updateSiteResource"; -export * from "./listSiteResources"; -export * from "./listAllSiteResourcesByOrg"; diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts deleted file mode 100644 index 948fc2c2..00000000 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { siteResources, sites, SiteResource } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; - -const listAllSiteResourcesByOrgParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const listAllSiteResourcesByOrgQuerySchema = z.object({ - limit: z - .string() - .optional() - .default("1000") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -export type ListAllSiteResourcesByOrgResponse = { - siteResources: (SiteResource & { siteName: string, siteNiceId: string })[]; -}; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/site-resources", - description: "List all site resources for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: listAllSiteResourcesByOrgParamsSchema, - query: listAllSiteResourcesByOrgQuerySchema - }, - responses: {} -}); - -export async function listAllSiteResourcesByOrg( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = listAllSiteResourcesByOrgParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedQuery = listAllSiteResourcesByOrgQuerySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - const { limit, offset } = parsedQuery.data; - - // Get all site resources for the org with site names - const siteResourcesList = await db - .select({ - siteResourceId: siteResources.siteResourceId, - siteId: siteResources.siteId, - orgId: siteResources.orgId, - name: siteResources.name, - protocol: siteResources.protocol, - proxyPort: siteResources.proxyPort, - destinationPort: siteResources.destinationPort, - destinationIp: siteResources.destinationIp, - enabled: siteResources.enabled, - siteName: sites.name, - siteNiceId: sites.niceId - }) - .from(siteResources) - .innerJoin(sites, eq(siteResources.siteId, sites.siteId)) - .where(eq(siteResources.orgId, orgId)) - .limit(limit) - .offset(offset); - - return response(res, { - data: { siteResources: siteResourcesList }, - success: true, - error: false, - message: "Site resources retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error("Error listing all site resources by org:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); - } -} diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts deleted file mode 100644 index 7fdb7a85..00000000 --- a/server/routers/siteResource/listSiteResources.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { siteResources, sites, SiteResource } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; - -const listSiteResourcesParamsSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number().int().positive()), - orgId: z.string() - }) - .strict(); - -const listSiteResourcesQuerySchema = z.object({ - limit: z - .string() - .optional() - .default("100") - .transform(Number) - .pipe(z.number().int().positive()), - offset: z - .string() - .optional() - .default("0") - .transform(Number) - .pipe(z.number().int().nonnegative()) -}); - -export type ListSiteResourcesResponse = { - siteResources: SiteResource[]; -}; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/site/{siteId}/resources", - description: "List site resources for a site.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: listSiteResourcesParamsSchema, - query: listSiteResourcesQuerySchema - }, - responses: {} -}); - -export async function listSiteResources( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = listSiteResourcesParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedQuery = listSiteResourcesQuerySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error).toString() - ) - ); - } - - const { siteId, orgId } = parsedParams.data; - const { limit, offset } = parsedQuery.data; - - // Verify the site exists and belongs to the org - const site = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (site.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Site not found" - ) - ); - } - - // Get site resources - const siteResourcesList = await db - .select() - .from(siteResources) - .where(and( - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - )) - .limit(limit) - .offset(offset); - - return response(res, { - data: { siteResources: siteResourcesList }, - success: true, - error: false, - message: "Site resources retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error("Error listing site resources:", error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); - } -} diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts deleted file mode 100644 index f6f71124..00000000 --- a/server/routers/siteResource/updateSiteResource.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, newts, sites } from "@server/db"; -import { siteResources, SiteResource } from "@server/db"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; -import { addTargets } from "../client/targets"; - -const updateSiteResourceParamsSchema = z - .object({ - siteResourceId: z - .string() - .transform(Number) - .pipe(z.number().int().positive()), - siteId: z.string().transform(Number).pipe(z.number().int().positive()), - orgId: z.string() - }) - .strict(); - -const updateSiteResourceSchema = z - .object({ - name: z.string().min(1).max(255).optional(), - protocol: z.enum(["tcp", "udp"]).optional(), - proxyPort: z.number().int().positive().optional(), - destinationPort: z.number().int().positive().optional(), - destinationIp: z.string().optional(), - enabled: z.boolean().optional() - }) - .strict(); - -export type UpdateSiteResourceBody = z.infer; -export type UpdateSiteResourceResponse = SiteResource; - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", - description: "Update a site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], - request: { - params: updateSiteResourceParamsSchema, - body: { - content: { - "application/json": { - schema: updateSiteResourceSchema - } - } - } - }, - responses: {} -}); - -export async function updateSiteResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = updateSiteResourceParamsSchema.safeParse( - req.params - ); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = updateSiteResourceSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { siteResourceId, siteId, orgId } = parsedParams.data; - const updateData = parsedBody.data; - - const [site] = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } - - // Check if site resource exists - const [existingSiteResource] = await db - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) - .limit(1); - - if (!existingSiteResource) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Site resource not found") - ); - } - - const protocol = updateData.protocol || existingSiteResource.protocol; - const proxyPort = - updateData.proxyPort || existingSiteResource.proxyPort; - - // check if resource with same protocol and proxy port already exists - const [existingResource] = await db - .select() - .from(siteResources) - .where( - and( - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId), - eq(siteResources.protocol, protocol), - eq(siteResources.proxyPort, proxyPort) - ) - ) - .limit(1); - if ( - existingResource && - existingResource.siteResourceId !== siteResourceId - ) { - return next( - createHttpError( - HttpCode.CONFLICT, - "A resource with the same protocol and proxy port already exists" - ) - ); - } - - // Update the site resource - const [updatedSiteResource] = await db - .update(siteResources) - .set(updateData) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) - .returning(); - - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); - } - - await addTargets( - newt.newtId, - updatedSiteResource.destinationIp, - updatedSiteResource.destinationPort, - updatedSiteResource.protocol, - updatedSiteResource.proxyPort - ); - - logger.info( - `Updated site resource ${siteResourceId} for site ${siteId}` - ); - - return response(res, { - data: updatedSiteResource, - success: true, - error: false, - message: "Site resource updated successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error("Error updating site resource:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to update site resource" - ) - ); - } -} diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 1aef3251..810ee409 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, TargetHealthCheck, targetHealthCheck } from "@server/db"; -import { newts, resources, sites, Target, targets } from "@server/db"; +import { db } from "@server/db"; +import { newts, resources, sites, Target, targets } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -26,48 +26,14 @@ const createTargetParamsSchema = z const createTargetSchema = z .object({ - siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), port: z.number().int().min(1).max(65535), - 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(), - rewritePath: z.string().optional().nullable(), - rewritePathType: z - .enum(["exact", "prefix", "regex", "stripPrefix"]) - .optional() - .nullable(), - priority: z.number().int().min(1).max(1000).optional().nullable() + enabled: z.boolean().default(true) }) .strict(); -export type CreateTargetResponse = Target & TargetHealthCheck; +export type CreateTargetResponse = Target; registry.registerPath({ method: "put", @@ -132,55 +98,28 @@ export async function createTarget( ); } - const siteId = targetData.siteId; - const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, siteId)) + .where(eq(sites.siteId, resource.siteId!)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` - ) - ); - } - - const existingTargets = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resourceId)); - - const existingTarget = existingTargets.find( - (target) => - target.ip === targetData.ip && - target.port === targetData.port && - target.method === targetData.method && - target.siteId === targetData.siteId - ); - - if (existingTarget) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}` + `Site with ID ${resource.siteId} not found` ) ); } let newTarget: Target[] = []; - let healthCheck: TargetHealthCheck[] = []; - let targetIps: string[] = []; if (site.type == "local") { newTarget = await db .insert(targets) .values({ resourceId, - ...targetData, - priority: targetData.priority || 100 + ...targetData }) .returning(); } else { @@ -197,10 +136,7 @@ export async function createTarget( ); } - const { internalPort, targetIps: newTargetIps } = await pickPort( - site.siteId!, - db - ); + const { internalPort, targetIps } = await pickPort(site.siteId!); if (!internalPort) { return next( @@ -215,81 +151,35 @@ export async function createTarget( .insert(targets) .values({ resourceId, - siteId: site.siteId, - ip: targetData.ip, - method: targetData.method, - port: targetData.port, internalPort, - enabled: targetData.enabled, - path: targetData.path, - pathMatchType: targetData.pathMatchType, - rewritePath: targetData.rewritePath, - rewritePathType: targetData.rewritePathType, - priority: targetData.priority || 100 + ...targetData }) .returning(); // add the new target to the targetIps array - newTargetIps.push(`${targetData.ip}/32`); + targetIps.push(`${targetData.ip}/32`); - targetIps = newTargetIps; - } + 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); - 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 - ); + addTargets(newt.newtId, newTarget, resource.protocol); + } } } return response(res, { - data: { - ...newTarget[0], - ...healthCheck[0] - }, + data: newTarget[0], success: true, error: false, message: "Target created successfully", diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 596691e4..979740dd 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; +import { newts, resources, sites, targets } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -76,38 +76,38 @@ export async function deleteTarget( ); } - // const [site] = await db - // .select() - // .from(sites) - // .where(eq(sites.siteId, resource.siteId!)) - // .limit(1); - // - // if (!site) { - // return next( - // createHttpError( - // HttpCode.NOT_FOUND, - // `Site with ID ${resource.siteId} not found` - // ) - // ); - // } - // - // if (site.pubKey) { - // if (site.type == "wireguard") { - // await addPeer(site.exitNodeId!, { - // publicKey: site.pubKey, - // allowedIps: await getAllowedIps(site.siteId) - // }); - // } else if (site.type == "newt") { - // // get the newt on the site by querying the newt table for siteId - // const [newt] = await db - // .select() - // .from(newts) - // .where(eq(newts.siteId, site.siteId)) - // .limit(1); - // - // removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); - // } - // } + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, resource.siteId!)) + .limit(1); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${resource.siteId} not found` + ) + ); + } + + if (site.pubKey) { + if (site.type == "wireguard") { + await addPeer(site.exitNodeId!, { + publicKey: site.pubKey, + allowedIps: await getAllowedIps(site.siteId) + }); + } else if (site.type == "newt") { + // get the newt on the site by querying the newt table for siteId + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + removeTargets(newt.newtId, [deletedTarget], resource.protocol); + } + } return response(res, { data: null, diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 864c02eb..a268629c 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Target, targetHealthCheck, TargetHealthCheck } from "@server/db"; -import { targets } from "@server/db"; +import { db } from "@server/db"; +import { targets } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -16,10 +16,6 @@ const getTargetSchema = z }) .strict(); -type GetTargetResponse = Target & Omit & { - hcHeaders: { name: string; value: string; }[] | null; -}; - registry.registerPath({ method: "get", path: "/target/{targetId}", @@ -64,29 +60,8 @@ 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], - ...targetHc, - hcHeaders: parsedHcHeaders - }, + return response(res, { + data: target[0], success: true, error: false, message: "Target retrieved successfully", diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts deleted file mode 100644 index ee4e7950..00000000 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ /dev/null @@ -1,114 +0,0 @@ -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/helpers.ts b/server/routers/target/helpers.ts index 13b2ee46..8fc8797f 100644 --- a/server/routers/target/helpers.ts +++ b/server/routers/target/helpers.ts @@ -1,29 +1,36 @@ -import { db, Transaction } from "@server/db"; -import { resources, targets } from "@server/db"; +import { db } from "@server/db"; +import { resources, targets } from "@server/db/schemas"; import { eq } from "drizzle-orm"; -const currentBannedPorts: number[] = []; +let currentBannedPorts: number[] = []; -export async function pickPort(siteId: number, trx: Transaction | typeof db): Promise<{ +export async function pickPort(siteId: number): Promise<{ internalPort: number; targetIps: string[]; }> { - // Fetch targets for all resources of this site - const targetIps: string[] = []; - const targetInternalPorts: number[] = []; - - const targetsRes = await trx - .select() - .from(targets) - .where(eq(targets.siteId, siteId)); - - targetsRes.forEach((target) => { - targetIps.push(`${target.ip}/32`); - if (target.internalPort) { - targetInternalPorts.push(target.internalPort); - } + // Fetch resources for this site + const resourcesRes = await db.query.resources.findMany({ + where: eq(resources.siteId, siteId) }); + // TODO: is this all inefficient? + // Fetch targets for all resources of this site + let targetIps: string[] = []; + let targetInternalPorts: number[] = []; + await Promise.all( + resourcesRes.map(async (resource) => { + const targetsRes = await db.query.targets.findMany({ + where: eq(targets.resourceId, resource.resourceId) + }); + targetsRes.forEach((target) => { + targetIps.push(`${target.ip}/32`); + if (target.internalPort) { + targetInternalPorts.push(target.internalPort); + } + }); + }) + ); + let internalPort!: number; // pick a port random port from 40000 to 65535 that is not in use for (let i = 0; i < 1000; i++) { @@ -35,20 +42,25 @@ export async function pickPort(siteId: number, trx: Transaction | typeof db): Pr break; } } - currentBannedPorts.push(internalPort); return { internalPort, targetIps }; } export async function getAllowedIps(siteId: number) { + // TODO: is this all inefficient? + const resourcesRes = await db.query.resources.findMany({ + where: eq(resources.siteId, siteId) + }); + // Fetch targets for all resources of this site - const targetsRes = await db - .select() - .from(targets) - .where(eq(targets.siteId, siteId)); - - const targetIps = targetsRes.map((target) => `${target.ip}/32`); - + const targetIps = await Promise.all( + resourcesRes.map(async (resource) => { + const targetsRes = await db.query.targets.findMany({ + where: eq(targets.resourceId, resource.resourceId) + }); + return targetsRes.map((target) => `${target.ip}/32`); + }) + ); return targetIps.flat(); } diff --git a/server/routers/target/index.ts b/server/routers/target/index.ts index 7d023bbd..b128edcd 100644 --- a/server/routers/target/index.ts +++ b/server/routers/target/index.ts @@ -2,5 +2,4 @@ export * from "./getTarget"; export * from "./createTarget"; export * from "./deleteTarget"; export * from "./updateTarget"; -export * from "./listTargets"; -export * from "./handleHealthcheckStatusMessage"; +export * from "./listTargets"; \ No newline at end of file diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 04966f6e..3d4c573b 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -1,5 +1,5 @@ -import { db, sites, targetHealthCheck } from "@server/db"; -import { targets } from "@server/db"; +import { db } from "@server/db"; +import { targets } from "@server/db/schemas"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { eq, sql } from "drizzle-orm"; @@ -35,53 +35,25 @@ const listTargetsSchema = z.object({ }); function queryTargets(resourceId: number) { - const baseQuery = db + let baseQuery = db .select({ targetId: targets.targetId, ip: targets.ip, method: targets.method, port: targets.port, enabled: targets.enabled, - 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, - rewritePath: targets.rewritePath, - rewritePathType: targets.rewritePathType, - priority: targets.priority, + resourceId: targets.resourceId + // resourceName: resources.name, }) .from(targets) - .leftJoin(sites, eq(sites.siteId, targets.siteId)) - .leftJoin( - targetHealthCheck, - eq(targetHealthCheck.targetId, targets.targetId) - ) + // .leftJoin(resources, eq(targets.resourceId, resources.resourceId)) .where(eq(targets.resourceId, resourceId)); return baseQuery; } -type TargetWithParsedHeaders = Omit>[0], 'hcHeaders'> & { - hcHeaders: { name: string; value: string; }[] | null; -}; - export type ListTargetsResponse = { - targets: TargetWithParsedHeaders[]; + targets: Awaited>; pagination: { total: number; limit: number; offset: number }; }; @@ -127,7 +99,7 @@ export async function listTargets( const baseQuery = queryTargets(resourceId); - const countQuery = db + let countQuery = db .select({ count: sql`cast(count(*) as integer)` }) .from(targets) .where(eq(targets.resourceId, resourceId)); @@ -136,26 +108,9 @@ 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: parsedTargetsList, + targets: targetsList, pagination: { total: totalCount, limit, diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index d332609d..284b1a31 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, targetHealthCheck } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; +import { db } from "@server/db"; +import { newts, resources, sites, targets } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -13,7 +13,6 @@ 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({ @@ -23,35 +22,10 @@ const updateTargetParamsSchema = z const updateTargetBodySchema = z .object({ - siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), - 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(), - rewritePath: z.string().optional().nullable(), - rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), - priority: z.number().int().min(1).max(1000).optional(), + enabled: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -103,7 +77,6 @@ export async function updateTarget( } const { targetId } = parsedParams.data; - const { siteId } = parsedBody.data; const [target] = await db .select() @@ -138,47 +111,19 @@ export async function updateTarget( const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, siteId)) + .where(eq(sites.siteId, resource.siteId!)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` + `Site with ID ${resource.siteId} not found` ) ); } - const targetData = { - ...target, - ...parsedBody.data - }; - - const existingTargets = await db - .select() - .from(targets) - .where(eq(targets.resourceId, target.resourceId)); - - const foundTarget = existingTargets.find( - (target) => - target.targetId !== targetId && // Exclude the current target being updated - target.ip === targetData.ip && - target.port === targetData.port && - target.method === targetData.method && - target.siteId === targetData.siteId - ); - - if (foundTarget) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Target with IP ${targetData.ip}, port ${targetData.port}, and method ${targetData.method} already exists on the same site.` - ) - ); - } - - const { internalPort, targetIps } = await pickPort(site.siteId!, db); + const { internalPort, targetIps } = await pickPort(site.siteId!); if (!internalPort) { return next( @@ -192,46 +137,12 @@ export async function updateTarget( const [updatedTarget] = await db .update(targets) .set({ - 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 + ...parsedBody.data, + internalPort }) .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!, { @@ -246,20 +157,11 @@ export async function updateTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - await addTargets( - newt.newtId, - [updatedTarget], - [updatedHc], - resource.protocol, - resource.proxyPort - ); + addTargets(newt.newtId, [updatedTarget], resource.protocol); } } return response(res, { - data: { - ...updatedTarget, - ...updatedHc - }, + data: updatedTarget, success: true, error: false, message: "Target updated successfully", diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts new file mode 100644 index 00000000..2fd656ba --- /dev/null +++ b/server/routers/traefik/getTraefikConfig.ts @@ -0,0 +1,416 @@ +import { Request, Response } from "express"; +import db from "@server/db"; +import { and, eq, inArray } 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/schemas"; +import { sql } from "drizzle-orm"; + +export async function traefikConfigProvider( + _: Request, + res: Response +): Promise { + try { + // Get all resources with related data + const allResources = await db.transaction(async (tx) => { + // First query to get resources with site and org info + const resourcesWithRelations = await tx + .select({ + // Resource fields + resourceId: resources.resourceId, + subdomain: resources.subdomain, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + blockAccess: resources.blockAccess, + sso: resources.sso, + emailWhitelistEnabled: resources.emailWhitelistEnabled, + http: resources.http, + proxyPort: resources.proxyPort, + protocol: resources.protocol, + isBaseDomain: resources.isBaseDomain, + domainId: resources.domainId, + // Site fields + site: { + siteId: sites.siteId, + type: sites.type, + subnet: sites.subnet + }, + // Org fields + org: { + orgId: orgs.orgId + }, + enabled: resources.enabled, + stickySession: resources.stickySession, + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader + }) + .from(resources) + .innerJoin(sites, eq(sites.siteId, resources.siteId)) + .innerJoin(orgs, eq(resources.orgId, orgs.orgId)); + + // Get all resource IDs from the first query + const resourceIds = resourcesWithRelations.map((r) => r.resourceId); + + // Second query to get all enabled targets for these resources + const allTargets = + resourceIds.length > 0 + ? await tx + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled + }) + .from(targets) + .where( + and( + inArray(targets.resourceId, resourceIds), + eq(targets.enabled, true) + ) + ) + : []; + + // Create a map for fast target lookup by resourceId + const targetsMap = allTargets.reduce((map, target) => { + if (!map.has(target.resourceId)) { + map.set(target.resourceId, []); + } + map.get(target.resourceId).push(target); + return map; + }, new Map()); + + // Combine the data + return resourcesWithRelations.map((resource) => ({ + ...resource, + targets: targetsMap.get(resource.resourceId) || [] + })); + }); + + if (!allResources.length) { + return res.status(HttpCode.OK).json({}); + } + + const badgerMiddlewareName = "badger"; + const redirectHttpsMiddlewareName = "redirect-to-https"; + + const config_output: any = { + http: { + middlewares: { + [badgerMiddlewareName]: { + plugin: { + [badgerMiddlewareName]: { + apiBaseUrl: new URL( + "/api/v1", + `http://${ + config.getRawConfig().server + .internal_hostname + }:${ + config.getRawConfig().server + .internal_port + }` + ).href, + userSessionCookieName: + config.getRawConfig().server + .session_cookie_name, + + // deprecated + accessTokenQueryParam: + config.getRawConfig().server + .resource_access_token_param, + + resourceSessionRequestParam: + config.getRawConfig().server + .resource_session_request_param + } + } + }, + [redirectHttpsMiddlewareName]: { + redirectScheme: { + scheme: "https" + } + } + } + } + }; + + for (const resource of allResources) { + const targets = resource.targets as Target[]; + const site = resource.site; + const org = resource.org; + + const routerName = `${resource.resourceId}-router`; + const serviceName = `${resource.resourceId}-service`; + const fullDomain = `${resource.fullDomain}`; + const transportName = `${resource.resourceId}-transport`; + const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; + + if (!resource.enabled) { + continue; + } + + if (resource.http) { + if (!resource.domainId) { + continue; + } + + if (!resource.fullDomain) { + logger.error( + `Resource ${resource.resourceId} has no fullDomain` + ); + continue; + } + + // HTTP configuration remains the same + if (!resource.subdomain && !resource.isBaseDomain) { + continue; + } + + // add routers and services empty objects if they don't exist + if (!config_output.http.routers) { + config_output.http.routers = {}; + } + + if (!config_output.http.services) { + config_output.http.services = {}; + } + + const domainParts = fullDomain.split("."); + let wildCard; + if (domainParts.length <= 2) { + wildCard = `*.${domainParts.join(".")}`; + } else { + wildCard = `*.${domainParts.slice(1).join(".")}`; + } + + if (resource.isBaseDomain) { + wildCard = resource.fullDomain; + } + + const configDomain = config.getDomain(resource.domainId); + + if (!configDomain) { + logger.error( + `Failed to get domain from config for resource ${resource.resourceId}` + ); + continue; + } + + const tls = { + certResolver: configDomain.cert_resolver, + ...(configDomain.prefer_wildcard_cert + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; + + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; + + config_output.http.routers![routerName] = { + entryPoints: [ + resource.ssl + ? config.getRawConfig().traefik.https_entrypoint + : config.getRawConfig().traefik.http_entrypoint + ], + middlewares: [ + badgerMiddlewareName, + ...additionalMiddlewares + ], + service: serviceName, + rule: `Host(\`${fullDomain}\`)`, + ...(resource.ssl ? { tls } : {}) + }; + + if (resource.ssl) { + config_output.http.routers![routerName + "-redirect"] = { + entryPoints: [ + config.getRawConfig().traefik.http_entrypoint + ], + middlewares: [redirectHttpsMiddlewareName], + service: serviceName, + rule: `Host(\`${fullDomain}\`)` + }; + } + + config_output.http.services![serviceName] = { + loadBalancer: { + servers: targets + .filter((target: Target) => { + if (!target.enabled) { + return false; + } + if ( + site.type === "local" || + site.type === "wireguard" + ) { + if ( + !target.ip || + !target.port || + !target.method + ) { + return false; + } + } else if (site.type === "newt") { + if ( + !target.internalPort || + !target.method + ) { + return false; + } + } + return true; + }) + .map((target: Target) => { + if ( + site.type === "local" || + site.type === "wireguard" + ) { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (site.type === "newt") { + const ip = site.subnet.split("/")[0]; + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }), + ...(resource.stickySession + ? { + sticky: { + cookie: { + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + secure: resource.ssl, + httpOnly: true + } + } + } + : {}) + } + }; + + // Add the serversTransport if TLS server name is provided + if (resource.tlsServerName) { + if (!config_output.http.serversTransports) { + config_output.http.serversTransports = {}; + } + config_output.http.serversTransports![transportName] = { + serverName: resource.tlsServerName, + //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings + // if defined in the static config and here. if not set, self-signed certs won't work + insecureSkipVerify: true + }; + config_output.http.services![serviceName].loadBalancer.serversTransport = transportName; + } + + // Add the host header middleware + if (resource.setHostHeader) { + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + config_output.http.middlewares[hostHeaderMiddlewareName] = + { + headers: { + customRequestHeaders: { + Host: resource.setHostHeader + } + } + }; + if (!config_output.http.routers![routerName].middlewares) { + config_output.http.routers![routerName].middlewares = []; + } + config_output.http.routers![routerName].middlewares = [ + ...config_output.http.routers![routerName].middlewares, + hostHeaderMiddlewareName + ]; + } + + } else { + // Non-HTTP (TCP/UDP) configuration + const protocol = resource.protocol.toLowerCase(); + const port = resource.proxyPort; + + if (!port) { + continue; + } + + if (!config_output[protocol]) { + config_output[protocol] = { + routers: {}, + services: {} + }; + } + + config_output[protocol].routers[routerName] = { + entryPoints: [`${protocol}-${port}`], + service: serviceName, + ...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {}) + }; + + config_output[protocol].services[serviceName] = { + loadBalancer: { + servers: targets + .filter((target: Target) => { + if (!target.enabled) { + return false; + } + if ( + site.type === "local" || + site.type === "wireguard" + ) { + if (!target.ip || !target.port) { + return false; + } + } else if (site.type === "newt") { + if (!target.internalPort) { + return false; + } + } + return true; + }) + .map((target: Target) => { + if ( + site.type === "local" || + site.type === "wireguard" + ) { + return { + address: `${target.ip}:${target.port}` + }; + } else if (site.type === "newt") { + const ip = site.subnet.split("/")[0]; + return { + address: `${ip}:${target.internalPort}` + }; + } + }), + ...(resource.stickySession + ? { + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } + : {}) + } + }; + } + } + return res.status(HttpCode.OK).json(config_output); + } 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/traefik/index.ts b/server/routers/traefik/index.ts index 6f5bd4f0..5630028c 100644 --- a/server/routers/traefik/index.ts +++ b/server/routers/traefik/index.ts @@ -1 +1 @@ -export * from "./traefikConfigProvider"; \ No newline at end of file +export * from "./getTraefikConfig"; \ No newline at end of file diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts deleted file mode 100644 index 89347932..00000000 --- a/server/routers/traefik/traefikConfigProvider.ts +++ /dev/null @@ -1,60 +0,0 @@ -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/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 73bed018..cc483b16 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, UserOrg } from "@server/db"; -import { roles, userInvites, userOrgs, users } from "@server/db"; +import { db } from "@server/db"; +import { roles, userInvites, userOrgs, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -92,7 +92,6 @@ export async function acceptInvite( } let roleId: number; - let totalUsers: UserOrg[] | undefined; // get the role to make sure it exists const existingRole = await db .select() @@ -123,12 +122,6 @@ export async function acceptInvite( await trx .delete(userInvites) .where(eq(userInvites.inviteId, inviteId)); - - // Get the total number of users in the org now - totalUsers = await db - .select() - .from(userOrgs) - .where(eq(userOrgs.orgId, existingInvite.orgId)); }); return response(res, { diff --git a/server/routers/user/addUserAction.ts b/server/routers/user/addUserAction.ts index 074ebe9b..472f4298 100644 --- a/server/routers/user/addUserAction.ts +++ b/server/routers/user/addUserAction.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userActions, users } from "@server/db"; +import { userActions, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/user/addUserRole.ts b/server/routers/user/addUserRole.ts index d073179d..b1c9025a 100644 --- a/server/routers/user/addUserRole.ts +++ b/server/routers/user/addUserRole.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userOrgs, roles } from "@server/db"; +import { userOrgs, roles } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -58,23 +58,18 @@ export async function addUserRole( ); } - // get the role - const [role] = await db - .select() - .from(roles) - .where(eq(roles.roleId, roleId)) - .limit(1); + const orgId = req.userOrg?.orgId || req.apiKeyOrg?.orgId; - if (!role) { + if (!orgId) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID") + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") ); } const existingUser = await db .select() .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId))) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); if (existingUser.length === 0) { @@ -98,7 +93,7 @@ export async function addUserRole( const roleExists = await db .select() .from(roles) - .where(and(eq(roles.roleId, roleId), eq(roles.orgId, role.orgId))) + .where(and(eq(roles.roleId, roleId), eq(roles.orgId, orgId))) .limit(1); if (roleExists.length === 0) { @@ -110,7 +105,7 @@ export async function addUserRole( ); } - const newUserRole = { orgId: role.orgId, userId, roleId, isOwner: false }; + const newUserRole = { orgId, userId, roleId, isOwner: false }; await db.transaction(async (trx) => { const hasRoleAlready = await trx @@ -119,7 +114,7 @@ export async function addUserRole( .where( and( eq(userOrgs.userId, userId), - eq(userOrgs.orgId, role.orgId), + eq(userOrgs.orgId, orgId), eq(userOrgs.roleId, roleId) ) ); diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index f094e20e..5b20ed8d 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, userResources, userSites } from "@server/db"; +import { resources, userResources, userSites } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -43,17 +43,17 @@ export async function addUserSite( }) .returning(); - // const siteResources = await trx - // .select() - // .from(resources) - // .where(eq(resources.siteId, siteId)); - // - // for (const resource of siteResources) { - // await trx.insert(userResources).values({ - // userId, - // resourceId: resource.resourceId - // }); - // } + const siteResources = await trx + .select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await trx.insert(userResources).values({ + userId, + resourceId: resource.resourceId + }); + } return response(res, { data: newUserSite[0], diff --git a/server/routers/user/adminGetUser.ts b/server/routers/user/adminGetUser.ts deleted file mode 100644 index 0a961bec..00000000 --- a/server/routers/user/adminGetUser.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { idp, users } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; - -const adminGetUserSchema = z - .object({ - userId: z.string().min(1) - }) - .strict(); - -registry.registerPath({ - method: "get", - path: "/user/{userId}", - description: "Get a user by ID.", - tags: [OpenAPITags.User], - request: { - params: adminGetUserSchema - }, - responses: {} -}); - -async function queryUser(userId: string) { - const [user] = await db - .select({ - userId: users.userId, - email: users.email, - username: users.username, - name: users.name, - type: users.type, - twoFactorEnabled: users.twoFactorEnabled, - twoFactorSetupRequested: users.twoFactorSetupRequested, - emailVerified: users.emailVerified, - serverAdmin: users.serverAdmin, - idpName: idp.name, - idpId: users.idpId, - dateCreated: users.dateCreated - }) - .from(users) - .leftJoin(idp, eq(users.idpId, idp.idpId)) - .where(eq(users.userId, userId)) - .limit(1); - return user; -} - -export type AdminGetUserResponse = NonNullable< - Awaited> ->; - -export async function adminGetUser( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = adminGetUserSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID") - ); - } - const { userId } = parsedParams.data; - - const user = await queryUser(userId); - - if (!user) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `User with ID ${userId} not found` - ) - ); - } - - return response(res, { - data: user, - success: true, - error: false, - message: "User retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index 308b9def..6de12be9 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { sql, eq } from "drizzle-orm"; import logger from "@server/logger"; -import { idp, users } from "@server/db"; +import { idp, users } from "@server/db/schemas"; import { fromZodError } from "zod-validation-error"; const listUsersSchema = z @@ -37,9 +37,7 @@ async function queryUsers(limit: number, offset: number) { serverAdmin: users.serverAdmin, type: users.type, idpName: idp.name, - idpId: users.idpId, - twoFactorEnabled: users.twoFactorEnabled, - twoFactorSetupRequested: users.twoFactorSetupRequested + idpId: users.idpId }) .from(users) .leftJoin(idp, eq(users.idpId, idp.idpId)) diff --git a/server/routers/user/adminRemoveUser.ts b/server/routers/user/adminRemoveUser.ts index 14916ab9..fa31c52f 100644 --- a/server/routers/user/adminRemoveUser.ts +++ b/server/routers/user/adminRemoveUser.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { users } from "@server/db"; +import { users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/adminUpdateUser2FA.ts b/server/routers/user/adminUpdateUser2FA.ts deleted file mode 100644 index becd2091..00000000 --- a/server/routers/user/adminUpdateUser2FA.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { users, userOrgs } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const updateUser2FAParamsSchema = z - .object({ - userId: z.string() - }) - .strict(); - -const updateUser2FABodySchema = z - .object({ - twoFactorSetupRequested: z.boolean() - }) - .strict(); - -export type UpdateUser2FAResponse = { - userId: string; - twoFactorRequested: boolean; -}; - -registry.registerPath({ - method: "post", - path: "/user/{userId}/2fa", - description: "Update a user's 2FA status.", - tags: [OpenAPITags.User], - request: { - params: updateUser2FAParamsSchema, - body: { - content: { - "application/json": { - schema: updateUser2FABodySchema - } - } - } - }, - responses: {} -}); - -export async function updateUser2FA( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = updateUser2FAParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = updateUser2FABodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { userId } = parsedParams.data; - const { twoFactorSetupRequested } = parsedBody.data; - - // Verify the user exists in the organization - const existingUser = await db - .select() - .from(users) - .where(eq(users.userId, userId)) - .limit(1); - - if (existingUser.length === 0) { - return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); - } - - if (existingUser[0].type !== "internal") { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Two-factor authentication is not supported for external users" - ) - ); - } - - logger.debug(`Updating 2FA for user ${userId} to ${twoFactorSetupRequested}`); - - if (twoFactorSetupRequested) { - await db - .update(users) - .set({ - twoFactorSetupRequested: true, - }) - .where(eq(users.userId, userId)); - } else { - await db - .update(users) - .set({ - twoFactorSetupRequested: false, - twoFactorEnabled: false, - twoFactorSecret: null - }) - .where(eq(users.userId, userId)); - } - - return response(res, { - data: { - userId: existingUser[0].userId, - twoFactorRequested: twoFactorSetupRequested - }, - success: true, - error: false, - message: `2FA ${twoFactorSetupRequested ? "enabled" : "disabled"} for user successfully`, - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index 5b11c923..a198db5d 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -6,9 +6,9 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { db, UserOrg } from "@server/db"; +import db from "@server/db"; import { and, eq } from "drizzle-orm"; -import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db"; +import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db/schemas"; import { generateId } from "@server/auth/sessions/app"; const paramsSchema = z @@ -21,7 +21,6 @@ const bodySchema = z .object({ email: z .string() - .toLowerCase() .optional() .refine((data) => { if (data) { @@ -29,7 +28,7 @@ const bodySchema = z } return true; }), - username: z.string().nonempty().toLowerCase(), + username: z.string().nonempty(), name: z.string().optional(), type: z.enum(["internal", "oidc"]).optional(), idpId: z.number().optional(), @@ -84,14 +83,7 @@ export async function createOrgUser( } const { orgId } = parsedParams.data; - const { - username, - email, - name, - type, - idpId, - roleId - } = parsedBody.data; + const { username, email, name, type, idpId, roleId } = parsedBody.data; const [role] = await db .select() @@ -142,82 +134,65 @@ export async function createOrgUser( ); } - let orgUsers: UserOrg[] | undefined; + const [existingUser] = await db + .select() + .from(users) + .where(eq(users.username, username)); - await db.transaction(async (trx) => { - const [existingUser] = await trx + if (existingUser) { + const [existingOrgUser] = await db .select() - .from(users) + .from(userOrgs) .where( and( - eq(users.username, username), - eq(users.idpId, idpId) + eq(userOrgs.orgId, orgId), + eq(userOrgs.userId, existingUser.userId) ) ); - if (existingUser) { - const [existingOrgUser] = await trx - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.orgId, orgId), - eq(userOrgs.userId, existingUser.userId) - ) - ); - - if (existingOrgUser) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "User already exists in this organization" - ) - ); - } - - await trx - .insert(userOrgs) - .values({ - orgId, - userId: existingUser.userId, - roleId: role.roleId, - autoProvisioned: false - }) - .returning(); - } else { - const userId = generateId(15); - - const [newUser] = await trx - .insert(users) - .values({ - userId: userId, - email, - username, - name, - type: "oidc", - idpId, - dateCreated: new Date().toISOString(), - emailVerified: true, - }) - .returning(); - - await trx - .insert(userOrgs) - .values({ - orgId, - userId: newUser.userId, - roleId: role.roleId, - autoProvisioned: false - }) - .returning(); + if (existingOrgUser) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User already exists in this organization" + ) + ); } - // List all of the users in the org - orgUsers = await trx - .select() - .from(userOrgs) - .where(eq(userOrgs.orgId, orgId)); - }); + await db + .insert(userOrgs) + .values({ + orgId, + userId: existingUser.userId, + roleId: role.roleId + }) + .returning(); + } else { + const userId = generateId(15); + + const [newUser] = await db + .insert(users) + .values({ + userId: userId, + email, + username, + name, + type: "oidc", + idpId, + dateCreated: new Date().toISOString(), + emailVerified: true + }) + .returning(); + + await db + .insert(userOrgs) + .values({ + orgId, + userId: newUser.userId, + roleId: role.roleId + }) + .returning(); + } } else { return next( createHttpError(HttpCode.BAD_REQUEST, "User type is required") diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index bdec2d12..226248a3 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, idp, idpOidcConfig } from "@server/db"; -import { roles, userOrgs, users } from "@server/db"; +import { db } from "@server/db"; +import { roles, userOrgs, users } from "@server/db/schemas"; import { and, eq, sql } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -23,20 +23,11 @@ async function queryUser(orgId: string, userId: string) { type: users.type, roles: sql`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, isOwner: userOrgs.isOwner, - isAdmin: roles.isAdmin, - twoFactorEnabled: users.twoFactorEnabled, - autoProvisioned: userOrgs.autoProvisioned, - idpId: users.idpId, - idpName: idp.name, - idpType: idp.type, - idpVariant: idpOidcConfig.variant, - idpAutoProvision: idp.autoProvision + isAdmin: roles.isAdmin }) .from(userOrgs) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(users, eq(userOrgs.userId, users.userId)) - .leftJoin(idp, eq(users.idpId, idp.idpId)) - .leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId)) .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); if (typeof user.roles === "string") { diff --git a/server/routers/user/getUser.ts b/server/routers/user/getUser.ts index e33daab6..2f80be90 100644 --- a/server/routers/user/getUser.ts +++ b/server/routers/user/getUser.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idp, users } from "@server/db"; +import { idp, users } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index a54ce681..a9400cdc 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -8,10 +8,6 @@ export * from "./acceptInvite"; export * from "./getOrgUser"; export * from "./adminListUsers"; export * from "./adminRemoveUser"; -export * from "./adminGetUser"; export * from "./listInvitations"; export * from "./removeInvitation"; export * from "./createOrgUser"; -export * from "./adminUpdateUser2FA"; -export * from "./adminGetUser"; -export * from "./updateOrgUser"; diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index d050a2fe..042942ab 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -2,7 +2,7 @@ import NodeCache from "node-cache"; import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs, userInvites, userOrgs, users } from "@server/db"; +import { orgs, userInvites, userOrgs, users } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -28,7 +28,10 @@ const inviteUserParamsSchema = z const inviteUserBodySchema = z .object({ - email: z.string().toLowerCase().email(), + email: z + .string() + .email() + .transform((v) => v.toLowerCase()), roleId: z.number(), validHours: z.number().gt(0).lte(168), sendEmail: z.boolean().optional(), @@ -185,7 +188,7 @@ export async function inviteUser( ) ); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; if (doEmail) { await sendEmail( @@ -237,7 +240,7 @@ export async function inviteUser( }); }); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; if (doEmail) { await sendEmail( diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index c91a136d..76e82db5 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userInvites, roles } from "@server/db"; +import { userInvites, roles } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 0535a79d..89752eb8 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, idpOidcConfig } from "@server/db"; -import { idp, roles, userOrgs, users } from "@server/db"; +import { db } from "@server/db"; +import { idp, roles, userOrgs, users } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -60,16 +60,12 @@ async function queryUsers(orgId: string, limit: number, offset: number) { roles: sql`json_group_array(json_object('id', ${roles.roleId}, 'name', ${roles.name}))`, isOwner: userOrgs.isOwner, idpName: idp.name, - idpId: users.idpId, - idpType: idp.type, - idpVariant: idpOidcConfig.variant, - twoFactorEnabled: users.twoFactorEnabled, + idpId: users.idpId }) .from(users) .leftJoin(userOrgs, eq(users.userId, userOrgs.userId)) .leftJoin(roles, eq(userOrgs.roleId, roles.roleId)) .leftJoin(idp, eq(users.idpId, idp.idpId)) - .leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId)) .where(eq(userOrgs.orgId, orgId)) .groupBy(users.userId) .limit(limit) diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index e3ee40d0..c825df6d 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userInvites } from "@server/db"; +import { userInvites } from "@server/db/schemas"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/removeUserAction.ts b/server/routers/user/removeUserAction.ts index f0bd7d92..9364f406 100644 --- a/server/routers/user/removeUserAction.ts +++ b/server/routers/user/removeUserAction.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userActions } from "@server/db"; +import { userActions } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index 960ef4da..b344978c 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -1,15 +1,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, count, eq, exists } from "drizzle-orm"; +import { db } from "@server/db"; +import { userOrgs, userResources, users, userSites } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { UserType } from "@server/types/UserTypes"; const removeUserSchema = z .object({ @@ -51,7 +50,7 @@ export async function removeUserOrg( const user = await db .select() .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))); + .where(eq(userOrgs.userId, userId)); if (!user || user.length === 0) { return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); @@ -66,8 +65,6 @@ export async function removeUserOrg( ); } - let userCount: UserOrg[] | undefined; - await db.transaction(async (trx) => { await trx .delete(userOrgs) @@ -75,47 +72,11 @@ export async function removeUserOrg( and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) ); - await db.delete(userResources).where( - and( - eq(userResources.userId, userId), - exists( - db - .select() - .from(resources) - .where( - and( - eq( - resources.resourceId, - userResources.resourceId - ), - eq(resources.orgId, orgId) - ) - ) - ) - ) - ); + await trx + .delete(userResources) + .where(eq(userResources.userId, userId)); - await db.delete(userSites).where( - and( - eq(userSites.userId, userId), - exists( - db - .select() - .from(sites) - .where( - and( - eq(sites.siteId, userSites.siteId), - eq(sites.orgId, orgId) - ) - ) - ) - ) - ); - - userCount = await trx - .select() - .from(userOrgs) - .where(eq(userOrgs.orgId, orgId)); + await trx.delete(userSites).where(eq(userSites.userId, userId)); }); return response(res, { diff --git a/server/routers/user/removeUserResource.ts b/server/routers/user/removeUserResource.ts index 186e8032..be5acab9 100644 --- a/server/routers/user/removeUserResource.ts +++ b/server/routers/user/removeUserResource.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userResources } from "@server/db"; +import { userResources } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 7dbb4a15..6142f45c 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, userResources, userSites } from "@server/db"; +import { resources, userResources, userSites } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -71,22 +71,22 @@ export async function removeUserSite( ); } - // const siteResources = await trx - // .select() - // .from(resources) - // .where(eq(resources.siteId, siteId)); - // - // for (const resource of siteResources) { - // await trx - // .delete(userResources) - // .where( - // and( - // eq(userResources.userId, userId), - // eq(userResources.resourceId, resource.resourceId) - // ) - // ) - // .returning(); - // } + const siteResources = await trx + .select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await trx + .delete(userResources) + .where( + and( + eq(userResources.userId, userId), + eq(userResources.resourceId, resource.resourceId) + ) + ) + .returning(); + } }); return response(res, { diff --git a/server/routers/user/setUserRoles.ts b/server/routers/user/setUserRoles.ts index 55a17180..e89c989b 100644 --- a/server/routers/user/setUserRoles.ts +++ b/server/routers/user/setUserRoles.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { userOrgs, roles } from "@server/db"; +import { userOrgs, roles } from "@server/db/schemas"; import { eq, and, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts deleted file mode 100644 index fb00b59f..00000000 --- a/server/routers/user/updateOrgUser.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, userOrgs } from "@server/db"; -import { and, eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; - -const paramsSchema = z - .object({ - userId: z.string(), - orgId: z.string() - }) - .strict(); - -const bodySchema = z - .object({ - autoProvisioned: z.boolean().optional() - }) - .strict() - .refine((data) => Object.keys(data).length > 0, { - message: "At least one field must be provided for update" - }); - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/user/{userId}", - description: "Update a user in an org.", - tags: [OpenAPITags.Org, OpenAPITags.User], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - -export async function updateOrgUser( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = paramsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = bodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { userId, orgId } = parsedParams.data; - - const [existingUser] = await db - .select() - .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (!existingUser) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "User not found in this organization" - ) - ); - } - - const updateData = parsedBody.data; - - const [updatedUser] = await db - .update(userOrgs) - .set({ - ...updateData - }) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .returning(); - - return response(res, { - data: updatedUser, - success: true, - error: false, - message: "Org user updated successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/ws.ts b/server/routers/ws.ts new file mode 100644 index 00000000..c4ee8874 --- /dev/null +++ b/server/routers/ws.ts @@ -0,0 +1,253 @@ +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 } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import db from "@server/db"; +import { validateNewtSessionToken } from "@server/auth/sessions/newt"; +import { messageHandlers } from "./messageHandlers"; +import logger from "@server/logger"; + +// Custom interfaces +interface WebSocketRequest extends IncomingMessage { + token?: string; +} + +interface AuthenticatedWebSocket extends WebSocket { + newt?: Newt; +} + +interface TokenPayload { + newt: Newt; + session: NewtSession; +} + +interface WSMessage { + type: string; + data: any; +} + +interface HandlerResponse { + message: WSMessage; + broadcast?: boolean; + excludeSender?: boolean; + targetNewtId?: string; +} + +interface HandlerContext { + message: WSMessage; + senderWs: WebSocket; + newt: Newt | undefined; + sendToClient: (newtId: string, message: WSMessage) => boolean; + broadcastToAllExcept: (message: WSMessage, excludeNewtId?: string) => void; + connectedClients: Map; +} + +export type MessageHandler = (context: HandlerContext) => Promise; + +const router: Router = Router(); +const wss: WebSocketServer = new WebSocketServer({ noServer: true }); + +// Client tracking map +let connectedClients: Map = new Map(); + +// Helper functions for client management +const addClient = (newtId: string, ws: AuthenticatedWebSocket): void => { + const existingClients = connectedClients.get(newtId) || []; + existingClients.push(ws); + connectedClients.set(newtId, existingClients); + logger.info(`Client added to tracking - Newt ID: ${newtId}, Total connections: ${existingClients.length}`); +}; + +const removeClient = (newtId: string, ws: AuthenticatedWebSocket): void => { + const existingClients = connectedClients.get(newtId) || []; + const updatedClients = existingClients.filter(client => client !== ws); + + if (updatedClients.length === 0) { + connectedClients.delete(newtId); + logger.info(`All connections removed for Newt ID: ${newtId}`); + } else { + connectedClients.set(newtId, updatedClients); + logger.info(`Connection removed - Newt ID: ${newtId}, Remaining connections: ${updatedClients.length}`); + } +}; + +// Helper functions for sending messages +const sendToClient = (newtId: string, message: WSMessage): boolean => { + const clients = connectedClients.get(newtId); + if (!clients || clients.length === 0) { + logger.info(`No active connections found for Newt ID: ${newtId}`); + return false; + } + + const messageString = JSON.stringify(message); + clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(messageString); + } + }); + return true; +}; + +const broadcastToAllExcept = (message: WSMessage, excludeNewtId?: string): void => { + connectedClients.forEach((clients, newtId) => { + if (newtId !== excludeNewtId) { + clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(message)); + } + }); + } + }); +}; + +// Token verification middleware (unchanged) +const verifyToken = async (token: string): Promise => { + try { + const { session, newt } = await validateNewtSessionToken(token); + + if (!session || !newt) { + return null; + } + + const existingNewt = await db + .select() + .from(newts) + .where(eq(newts.newtId, newt.newtId)); + + if (!existingNewt || !existingNewt[0]) { + return null; + } + + return { newt: existingNewt[0], session }; + } catch (error) { + logger.error("Token verification failed:", error); + return null; + } +}; + +const setupConnection = (ws: AuthenticatedWebSocket, newt: Newt): void => { + logger.info("Establishing websocket connection"); + + if (!newt) { + logger.error("Connection attempt without newt"); + return ws.terminate(); + } + + ws.newt = newt; + + // Add client to tracking + addClient(newt.newtId, ws); + + ws.on("message", async (data) => { + try { + const message: WSMessage = JSON.parse(data.toString()); + // logger.info(`Message received from Newt ID ${newtId}:`, message); + + // Validate message format + if (!message.type || typeof message.type !== "string") { + throw new Error("Invalid message format: missing or invalid type"); + } + + // Get the appropriate handler for the message type + const handler = messageHandlers[message.type]; + if (!handler) { + throw new Error(`Unsupported message type: ${message.type}`); + } + + // Process the message and get response + const response = await handler({ + message, + senderWs: ws, + newt: ws.newt, + sendToClient, + broadcastToAllExcept, + connectedClients + }); + + // Send response if one was returned + if (response) { + if (response.broadcast) { + // Broadcast to all clients except sender if specified + broadcastToAllExcept(response.message, response.excludeSender ? newt.newtId : undefined); + } else if (response.targetNewtId) { + // Send to specific client if targetNewtId is provided + sendToClient(response.targetNewtId, response.message); + } else { + // Send back to sender + ws.send(JSON.stringify(response.message)); + } + } + + } catch (error) { + logger.error("Message handling error:", error); + ws.send(JSON.stringify({ + type: "error", + data: { + message: error instanceof Error ? error.message : "Unknown error occurred", + originalMessage: data.toString() + } + })); + } + }); + + ws.on("close", () => { + removeClient(newt.newtId, ws); + logger.info(`Client disconnected - Newt ID: ${newt.newtId}`); + }); + + ws.on("error", (error: Error) => { + logger.error(`WebSocket error for Newt ID ${newt.newtId}:`, error); + }); + + logger.info(`WebSocket connection established - Newt ID: ${newt.newtId}`); +}; + +// Router endpoint (unchanged) +router.get("/ws", (req: Request, res: Response) => { + res.status(200).send("WebSocket endpoint"); +}); + +// WebSocket upgrade handler +const handleWSUpgrade = (server: HttpServer): void => { + server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => { + try { + const token = request.url?.includes("?") + ? new URLSearchParams(request.url.split("?")[1]).get("token") || "" + : request.headers["sec-websocket-protocol"]; + + if (!token) { + logger.warn("Unauthorized connection attempt: no token..."); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + const tokenPayload = await verifyToken(token); + if (!tokenPayload) { + logger.warn("Unauthorized connection attempt: invalid token..."); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => { + setupConnection(ws, tokenPayload.newt); + }); + } catch (error) { + logger.error("WebSocket upgrade error:", error); + socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); + socket.destroy(); + } + }); +}; + +export { + router, + handleWSUpgrade, + sendToClient, + broadcastToAllExcept, + connectedClients +}; diff --git a/server/routers/ws/client.ts b/server/routers/ws/client.ts deleted file mode 100644 index 13b5d0da..00000000 --- a/server/routers/ws/client.ts +++ /dev/null @@ -1,315 +0,0 @@ -import WebSocket from 'ws'; -import axios from 'axios'; -import { URL } from 'url'; -import { EventEmitter } from 'events'; -import logger from '@server/logger'; - -export interface Config { - id: string; - secret: string; - endpoint: string; -} - -export interface WSMessage { - type: string; - data: any; -} - -export type MessageHandler = (message: WSMessage) => void; - -export interface ClientOptions { - baseURL?: string; - reconnectInterval?: number; - pingInterval?: number; - pingTimeout?: number; -} - -export class WebSocketClient extends EventEmitter { - private conn: WebSocket | null = null; - private baseURL: string; - private handlers: Map = new Map(); - private reconnectInterval: number; - private isConnected: boolean = false; - private pingInterval: number; - private pingTimeout: number; - private shouldReconnect: boolean = true; - private reconnectTimer: NodeJS.Timeout | null = null; - private pingTimer: NodeJS.Timeout | null = null; - private pingTimeoutTimer: NodeJS.Timeout | null = null; - private token: string; - private isConnecting: boolean = false; - - constructor( - token: string, - endpoint: string, - options: ClientOptions = {} - ) { - super(); - - this.token = token; - this.baseURL = options.baseURL || endpoint; - this.reconnectInterval = options.reconnectInterval || 5000; - this.pingInterval = options.pingInterval || 30000; - this.pingTimeout = options.pingTimeout || 10000; - } - - public async connect(): Promise { - this.shouldReconnect = true; - if (!this.isConnecting) { - await this.connectWithRetry(); - } - } - - public async close(): Promise { - this.shouldReconnect = false; - - // Clear timers - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - if (this.pingTimer) { - clearInterval(this.pingTimer); - this.pingTimer = null; - } - if (this.pingTimeoutTimer) { - clearTimeout(this.pingTimeoutTimer); - this.pingTimeoutTimer = null; - } - - if (this.conn) { - this.conn.close(1000, 'Client closing'); - this.conn = null; - } - - this.setConnected(false); - } - - public sendMessage(messageType: string, data: any): Promise { - return new Promise((resolve, reject) => { - if (!this.conn || this.conn.readyState !== WebSocket.OPEN) { - reject(new Error('Not connected')); - return; - } - - const message: WSMessage = { - type: messageType, - data: data - }; - - logger.debug(`Sending message: ${messageType}`, data); - - this.conn.send(JSON.stringify(message), (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } - - public sendMessageInterval( - messageType: string, - data: any, - interval: number - ): () => void { - // Send immediately - this.sendMessage(messageType, data).catch(err => { - logger.error('Failed to send initial message:', err); - }); - - // Set up interval - const intervalId = setInterval(() => { - this.sendMessage(messageType, data).catch(err => { - logger.error('Failed to send message:', err); - }); - }, interval); - - // Return stop function - return () => { - clearInterval(intervalId); - }; - } - - public registerHandler(messageType: string, handler: MessageHandler): void { - this.handlers.set(messageType, handler); - } - - public unregisterHandler(messageType: string): void { - this.handlers.delete(messageType); - } - - public isClientConnected(): boolean { - return this.isConnected; - } - - private async connectWithRetry(): Promise { - if (this.isConnecting || this.isConnected) return; - - this.isConnecting = true; - - while (this.shouldReconnect && !this.isConnected && this.isConnecting) { - try { - await this.establishConnection(); - this.isConnecting = false; - return; - } catch (error) { - logger.error(`Failed to connect: ${error}. Retrying in ${this.reconnectInterval}ms...`); - - if (!this.shouldReconnect || !this.isConnecting) { - this.isConnecting = false; - return; - } - - await new Promise(resolve => { - this.reconnectTimer = setTimeout(resolve, this.reconnectInterval); - }); - } - } - - this.isConnecting = false; - } - - private async establishConnection(): Promise { - // Clean up any existing connection before establishing a new one - if (this.conn) { - this.conn.removeAllListeners(); - this.conn.close(); - this.conn = null; - } - - // Parse the base URL to determine protocol and hostname - const baseURL = new URL(this.baseURL); - const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws'; - const wsURL = new URL(`${wsProtocol}://${baseURL.host}/api/v1/ws`); - - // Add token and client type to query parameters - wsURL.searchParams.set('token', this.token); - wsURL.searchParams.set('clientType', "remoteExitNode"); - - return new Promise((resolve, reject) => { - const conn = new WebSocket(wsURL.toString()); - - conn.on('open', () => { - logger.debug('WebSocket connection established'); - this.conn = conn; - this.setConnected(true); - this.isConnecting = false; - this.startPingMonitor(); - this.emit('connect'); - resolve(); - }); - - conn.on('message', (data: WebSocket.Data) => { - try { - const message: WSMessage = JSON.parse(data.toString()); - const handler = this.handlers.get(message.type); - if (handler) { - handler(message); - } - this.emit('message', message); - } catch (error) { - logger.error('Failed to parse message:', error); - } - }); - - conn.on('close', (code, reason) => { - logger.debug(`WebSocket connection closed: ${code} ${reason}`); - this.handleDisconnect(); - }); - - conn.on('error', (error) => { - logger.error('WebSocket error:', error); - if (this.conn === null) { - // Connection failed during establishment - reject(error); - } - // Don't call handleDisconnect here as the 'close' event will handle it - }); - - conn.on('pong', () => { - if (this.pingTimeoutTimer) { - clearTimeout(this.pingTimeoutTimer); - this.pingTimeoutTimer = null; - } - }); - }); - } - - private startPingMonitor(): void { - // Clear any existing ping timer to prevent duplicates - if (this.pingTimer) { - clearInterval(this.pingTimer); - this.pingTimer = null; - } - - this.pingTimer = setInterval(() => { - if (this.conn && this.conn.readyState === WebSocket.OPEN) { - this.conn.ping(); - - // Set timeout for pong response - this.pingTimeoutTimer = setTimeout(() => { - logger.error('Ping timeout - no pong received'); - this.handleDisconnect(); - }, this.pingTimeout); - } - }, this.pingInterval); - } - - private handleDisconnect(): void { - // Prevent multiple disconnect handlers from running simultaneously - if (!this.isConnected && !this.isConnecting) { - return; - } - - this.setConnected(false); - this.isConnecting = false; - - // Clear ping timers - if (this.pingTimer) { - clearInterval(this.pingTimer); - this.pingTimer = null; - } - if (this.pingTimeoutTimer) { - clearTimeout(this.pingTimeoutTimer); - this.pingTimeoutTimer = null; - } - - // Clear any existing reconnect timer to prevent multiple reconnection attempts - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - - if (this.conn) { - this.conn.removeAllListeners(); - this.conn = null; - } - - this.emit('disconnect'); - - // Reconnect if needed - if (this.shouldReconnect) { - // Add a small delay before starting reconnection to prevent immediate retry - this.reconnectTimer = setTimeout(() => { - this.connectWithRetry(); - }, 1000); - } - } - - private setConnected(status: boolean): void { - this.isConnected = status; - } -} - -// Factory function for easier instantiation -export function createWebSocketClient( - token: string, - endpoint: string, - options?: ClientOptions -): WebSocketClient { - return new WebSocketClient(token, endpoint, options); -} - -export default WebSocketClient; \ No newline at end of file diff --git a/server/routers/ws/index.ts b/server/routers/ws/index.ts deleted file mode 100644 index 16440ec9..00000000 --- a/server/routers/ws/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index cbb023b3..00000000 --- a/server/routers/ws/messageHandlers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - handleNewtRegisterMessage, - handleReceiveBandwidthMessage, - handleGetConfigMessage, - handleDockerStatusMessage, - handleDockerContainersMessage, - handleNewtPingRequestMessage, - handleApplyBlueprintMessage -} from "../newt"; -import { - handleOlmRegisterMessage, - handleOlmRelayMessage, - handleOlmPingMessage, - startOlmOfflineChecker -} from "../olm"; -import { handleHealthcheckStatusMessage } from "../target"; -import { MessageHandler } from "./types"; - -export const messageHandlers: Record = { - "newt/wg/register": handleNewtRegisterMessage, - "olm/wg/register": handleOlmRegisterMessage, - "newt/wg/get-config": handleGetConfigMessage, - "newt/receive-bandwidth": handleReceiveBandwidthMessage, - "olm/wg/relay": handleOlmRelayMessage, - "olm/ping": handleOlmPingMessage, - "newt/socket/status": handleDockerStatusMessage, - "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 \ No newline at end of file diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts deleted file mode 100644 index 7063bc87..00000000 --- a/server/routers/ws/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 9bba41dc..00000000 --- a/server/routers/ws/ws.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { Router, Request, Response } from "express"; -import { Server as HttpServer } from "http"; -import { WebSocket, WebSocketServer } from "ws"; -import { Socket } from "net"; -import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db"; -import { eq } from "drizzle-orm"; -import { db } from "@server/db"; -import { validateNewtSessionToken } from "@server/auth/sessions/newt"; -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"; - -// Subset of TokenPayload for public ws.ts (newt and olm only) -interface PublicTokenPayload { - client: Newt | Olm; - session: NewtSession | OlmSession; - clientType: "newt" | "olm"; -} - -const router: Router = Router(); -const wss: WebSocketServer = new WebSocketServer({ noServer: true }); - -// Generate unique node ID for this instance -const NODE_ID = uuidv4(); - -// Client tracking map (local to this node) -const connectedClients: Map = new Map(); -// Helper to get map key -const getClientMapKey = (clientId: string) => clientId; - -// Helper functions for client management -const addClient = async (clientType: ClientType, clientId: string, ws: AuthenticatedWebSocket): Promise => { - // Generate unique connection ID - const connectionId = uuidv4(); - ws.connectionId = connectionId; - - // Add to local tracking - const mapKey = getClientMapKey(clientId); - const existingClients = connectedClients.get(mapKey) || []; - existingClients.push(ws); - connectedClients.set(mapKey, existingClients); - - logger.info(`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}`); -}; - -const removeClient = async (clientType: ClientType, clientId: string, ws: AuthenticatedWebSocket): Promise => { - const mapKey = getClientMapKey(clientId); - const existingClients = connectedClients.get(mapKey) || []; - const updatedClients = existingClients.filter(client => client !== ws); - if (updatedClients.length === 0) { - connectedClients.delete(mapKey); - - logger.info(`All connections removed for ${clientType.toUpperCase()} ID: ${clientId}`); - } else { - connectedClients.set(mapKey, updatedClients); - - logger.info(`Connection removed - ${clientType.toUpperCase()} ID: ${clientId}, Remaining connections: ${updatedClients.length}`); - } -}; - -// Local message sending (within this node) -const sendToClientLocal = async (clientId: string, message: WSMessage): Promise => { - const mapKey = getClientMapKey(clientId); - const clients = connectedClients.get(mapKey); - if (!clients || clients.length === 0) { - return false; - } - const messageString = JSON.stringify(message); - clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(messageString); - } - }); - return true; -}; - -const broadcastToAllExceptLocal = async (message: WSMessage, excludeClientId?: string): Promise => { - connectedClients.forEach((clients, mapKey) => { - const [type, id] = mapKey.split(":"); - if (!(excludeClientId && id === excludeClientId)) { - clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(message)); - } - }); - } - }); -}; - -// Cross-node message sending -const sendToClient = async (clientId: string, message: WSMessage): Promise => { - // Try to send locally first - const localSent = await sendToClientLocal(clientId, message); - - return localSent; -}; - -const broadcastToAllExcept = async (message: WSMessage, excludeClientId?: string): Promise => { - // Broadcast locally - await broadcastToAllExceptLocal(message, excludeClientId); -}; - -// Check if a client has active connections across all nodes -const hasActiveConnections = async (clientId: string): Promise => { - const mapKey = getClientMapKey(clientId); - const clients = connectedClients.get(mapKey); - return !!(clients && clients.length > 0); -}; - -// Get all active nodes for a client -const getActiveNodes = async (clientType: ClientType, clientId: string): Promise => { - const mapKey = getClientMapKey(clientId); - const clients = connectedClients.get(mapKey); - return (clients && clients.length > 0) ? [NODE_ID] : []; -}; - -// Token verification middleware -const verifyToken = async (token: string, clientType: ClientType): Promise => { - -try { - if (clientType === 'newt') { - const { session, newt } = await validateNewtSessionToken(token); - if (!session || !newt) { - return null; - } - const existingNewt = await db - .select() - .from(newts) - .where(eq(newts.newtId, newt.newtId)); - if (!existingNewt || !existingNewt[0]) { - return null; - } - return { client: existingNewt[0], session, clientType }; - } else if (clientType === 'olm') { - const { session, olm } = await validateOlmSessionToken(token); - if (!session || !olm) { - return null; - } - const existingOlm = await db - .select() - .from(olms) - .where(eq(olms.olmId, olm.olmId)); - if (!existingOlm || !existingOlm[0]) { - return null; - } - 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: "newt" | "olm"): Promise => { - logger.info("Establishing websocket connection"); - if (!client) { - logger.error("Connection attempt without client"); - return ws.terminate(); - } - - ws.client = client; - ws.clientType = clientType; - - // Add client to tracking - const clientId = clientType === 'newt' ? (client as Newt).newtId : (client as Olm).olmId; - await addClient(clientType, clientId, ws); - - ws.on("message", async (data) => { - try { - const message: WSMessage = JSON.parse(data.toString()); - - if (!message.type || typeof message.type !== "string") { - throw new Error("Invalid message format: missing or invalid type"); - } - - const handler = messageHandlers[message.type]; - if (!handler) { - throw new Error(`Unsupported message type: ${message.type}`); - } - - const response = await handler({ - message, - senderWs: ws, - client: ws.client, - clientType: ws.clientType!, - sendToClient, - broadcastToAllExcept, - connectedClients - }); - - if (response) { - if (response.broadcast) { - await broadcastToAllExcept( - response.message, - response.excludeSender ? clientId : undefined - ); - } else if (response.targetClientId) { - await sendToClient(response.targetClientId, response.message); - } else { - ws.send(JSON.stringify(response.message)); - } - } - } catch (error) { - logger.error("Message handling error:", error); - ws.send(JSON.stringify({ - type: "error", - data: { - message: error instanceof Error ? error.message : "Unknown error occurred", - originalMessage: data.toString() - } - })); - } - }); - - ws.on("close", () => { - removeClient(clientType, clientId, ws); - logger.info(`Client disconnected - ${clientType.toUpperCase()} ID: ${clientId}`); - }); - - ws.on("error", (error: Error) => { - logger.error(`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`, error); - }); - - logger.info(`WebSocket connection established - ${clientType.toUpperCase()} ID: ${clientId}`); -}; - -// Router endpoint -router.get("/ws", (req: Request, res: Response) => { - res.status(200).send("WebSocket endpoint"); -}); - -// WebSocket upgrade handler -const handleWSUpgrade = (server: HttpServer): void => { - server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => { - try { - const url = new URL(request.url || '', `http://${request.headers.host}`); - const token = url.searchParams.get('token') || request.headers["sec-websocket-protocol"] || ''; - let clientType = url.searchParams.get('clientType') as ClientType; - - if (!clientType) { - clientType = "newt"; - } - - if (!token || !clientType || !['newt', 'olm'].includes(clientType)) { - logger.warn("Unauthorized connection attempt: invalid token or client type..."); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - - const tokenPayload = await verifyToken(token, clientType); - if (!tokenPayload) { - logger.warn("Unauthorized connection attempt: invalid token..."); - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - - wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => { - setupConnection(ws, tokenPayload.client, tokenPayload.clientType); - }); - } catch (error) { - logger.error("WebSocket upgrade error:", error); - socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); - socket.destroy(); - } - }); -}; - -// Cleanup function for graceful shutdown -const cleanup = async (): Promise => { - try { - // Close all WebSocket connections - connectedClients.forEach((clients) => { - clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.terminate(); - } - }); - }); - - logger.info('WebSocket cleanup completed'); - } catch (error) { - logger.error('Error during WebSocket cleanup:', error); - } -}; - -export { - router, - handleWSUpgrade, - sendToClient, - broadcastToAllExcept, - connectedClients, - hasActiveConnections, - getActiveNodes, - NODE_ID, - cleanup -}; diff --git a/server/setup/clearStaleData.ts b/server/setup/clearStaleData.ts index 0140b7b3..4d95107e 100644 --- a/server/setup/clearStaleData.ts +++ b/server/setup/clearStaleData.ts @@ -1,4 +1,4 @@ -import { db, sessionTransferToken } from "@server/db"; +import { db } from "@server/db"; import { emailVerificationCodes, newtSessions, @@ -8,7 +8,7 @@ import { resourceSessions, sessions, userInvites -} from "@server/db"; +} from "@server/db/schemas"; import logger from "@server/logger"; import { lt } from "drizzle-orm"; @@ -76,14 +76,4 @@ 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/copyInConfig.ts b/server/setup/copyInConfig.ts index b8c00192..ec5a137b 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -1,5 +1,5 @@ import { db } from "@server/db"; -import { domains, exitNodes, orgDomains, orgs, resources } from "@server/db"; +import { domains, exitNodes, orgDomains, orgs, resources } from "../db/schemas/schema"; import config from "@server/lib/config"; import { eq, ne } from "drizzle-orm"; import logger from "@server/logger"; @@ -8,31 +8,8 @@ export async function copyInConfig() { const endpoint = config.getRawConfig().gerbil.base_endpoint; const listenPort = config.getRawConfig().gerbil.start_port; - if (!config.getRawConfig().flags?.disable_config_managed_domains && config.getRawConfig().domains) { - await copyInDomains(); - } - - const exitNodeName = config.getRawConfig().gerbil.exit_node_name; - if (exitNodeName) { - await db - .update(exitNodes) - .set({ endpoint, listenPort }) - .where(eq(exitNodes.name, exitNodeName)); - } else { - await db - .update(exitNodes) - .set({ endpoint }) - .where(ne(exitNodes.endpoint, endpoint)); - await db - .update(exitNodes) - .set({ listenPort }) - .where(ne(exitNodes.listenPort, listenPort)); - } -} - -async function copyInDomains() { await db.transaction(async (trx) => { - const rawDomains = config.getRawConfig().domains!; // always defined if disable flag is not set + const rawDomains = config.getRawConfig().domains; const configDomains = Object.entries(rawDomains).map( ([key, value]) => ({ @@ -63,19 +40,13 @@ async function copyInDomains() { if (existingDomainKeys.has(domainId)) { await trx .update(domains) - .set({ baseDomain, verified: true, type: "wildcard" }) + .set({ baseDomain }) .where(eq(domains.domainId, domainId)) .execute(); } else { await trx .insert(domains) - .values({ - domainId, - baseDomain, - configManaged: true, - type: "wildcard", - verified: true - }) + .values({ domainId, baseDomain, configManaged: true }) .execute(); } } @@ -121,7 +92,7 @@ async function copyInDomains() { } let fullDomain = ""; - if (!resource.subdomain) { + if (resource.isBaseDomain) { fullDomain = domain.baseDomain; } else { fullDomain = `${resource.subdomain}.${domain.baseDomain}`; @@ -133,4 +104,15 @@ async function copyInDomains() { .where(eq(resources.resourceId, resource.resourceId)); } }); + + // TODO: eventually each exit node could have a different endpoint + await db + .update(exitNodes) + .set({ endpoint }) + .where(ne(exitNodes.endpoint, endpoint)); + // TODO: eventually each exit node could have a different port + await db + .update(exitNodes) + .set({ listenPort }) + .where(ne(exitNodes.listenPort, listenPort)); } diff --git a/server/setup/ensureActions.ts b/server/setup/ensureActions.ts index 7fd5384a..0d789e1d 100644 --- a/server/setup/ensureActions.ts +++ b/server/setup/ensureActions.ts @@ -1,6 +1,6 @@ import { ActionsEnum } from "@server/auth/actions"; import { db } from "@server/db"; -import { actions, roles, roleActions } from "@server/db"; +import { actions, roles, roleActions } from "../db/schemas/schema"; import { eq, inArray } from "drizzle-orm"; import logger from "@server/logger"; @@ -22,37 +22,85 @@ export async function ensureActions() { .where(eq(roles.isAdmin, true)) .execute(); - await db.transaction(async (trx) => { - // Add new actions - for (const actionId of actionsToAdd) { - logger.debug(`Adding action: ${actionId}`); - await trx.insert(actions).values({ actionId }).execute(); - // Add new actions to the Default role - if (defaultRoles.length != 0) { - await trx - .insert(roleActions) - .values( - defaultRoles.map((role) => ({ - roleId: role.roleId!, - actionId, - orgId: role.orgId! - })) - ) - .execute(); - } - } + await db.transaction(async (trx) => { - // Remove deprecated actions - if (actionsToRemove.length > 0) { - logger.debug(`Removing actions: ${actionsToRemove.join(", ")}`); + // Add new actions + for (const actionId of actionsToAdd) { + logger.debug(`Adding action: ${actionId}`); + await trx.insert(actions).values({ actionId }).execute(); + // Add new actions to the Default role + if (defaultRoles.length != 0) { await trx - .delete(actions) - .where(inArray(actions.actionId, actionsToRemove)) - .execute(); - await trx - .delete(roleActions) - .where(inArray(roleActions.actionId, actionsToRemove)) + .insert(roleActions) + .values( + defaultRoles.map((role) => ({ + roleId: role.roleId!, + actionId, + orgId: role.orgId! + })) + ) .execute(); } - }); + } + + // Remove deprecated actions + if (actionsToRemove.length > 0) { + logger.debug(`Removing actions: ${actionsToRemove.join(", ")}`); + await trx + .delete(actions) + .where(inArray(actions.actionId, actionsToRemove)) + .execute(); + await trx + .delete(roleActions) + .where(inArray(roleActions.actionId, actionsToRemove)) + .execute(); + } +}); +} + +export async function createAdminRole(orgId: string) { + let roleId: any; + await db.transaction(async (trx) => { + + const [insertedRole] = await trx + .insert(roles) + .values({ + orgId, + isAdmin: true, + name: "Admin", + description: "Admin role with the most permissions" + }) + .returning({ roleId: roles.roleId }) + .execute(); + + if (!insertedRole || !insertedRole.roleId) { + throw new Error("Failed to create Admin role"); + } + + roleId = insertedRole.roleId; + + const actionIds = await trx.select().from(actions).execute(); + + if (actionIds.length === 0) { + logger.info("No actions to assign to the Admin role"); + return; + } + + await trx + .insert(roleActions) + .values( + actionIds.map((action) => ({ + roleId, + actionId: action.actionId, + orgId + })) + ) + .execute(); + }); + + if (!roleId) { + throw new Error("Failed to create Admin role"); + } + + return roleId; } diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts deleted file mode 100644 index 1734b5e6..00000000 --- a/server/setup/ensureSetupToken.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { db, setupTokens, users } from "@server/db"; -import { eq } from "drizzle-orm"; -import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; -import moment from "moment"; -import logger from "@server/logger"; - -const random: RandomReader = { - read(bytes: Uint8Array): void { - crypto.getRandomValues(bytes); - } -}; - -function generateToken(): string { - // Generate a 32-character alphanumeric token - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - return generateRandomString(random, alphabet, 32); -} - -function generateId(length: number): string { - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - return generateRandomString(random, alphabet, length); -} - -export async function ensureSetupToken() { - try { - // Check if a server admin already exists - const [existingAdmin] = await db - .select() - .from(users) - .where(eq(users.serverAdmin, true)); - - // If admin exists, no need for setup token - if (existingAdmin) { - logger.warn("Server admin exists. Setup token generation skipped."); - return; - } - - // Check if a setup token already exists - const existingTokens = await db - .select() - .from(setupTokens) - .where(eq(setupTokens.used, false)); - - // If unused token exists, display it instead of creating a new one - if (existingTokens.length > 0) { - console.log("=== SETUP TOKEN EXISTS ==="); - console.log("Token:", existingTokens[0].token); - console.log("Use this token on the initial setup page"); - console.log("================================"); - return; - } - - // Generate a new setup token - const token = generateToken(); - const tokenId = generateId(15); - - await db.insert(setupTokens).values({ - tokenId: tokenId, - token: token, - used: false, - dateCreated: moment().toISOString(), - dateUsed: null - }); - - console.log("=== SETUP TOKEN GENERATED ==="); - console.log("Token:", token); - console.log("Use this token on the initial setup page"); - console.log("================================"); - } catch (error) { - console.error("Failed to ensure setup token:", error); - throw error; - } -} \ No newline at end of file diff --git a/server/setup/index.ts b/server/setup/index.ts index 2dfb633e..b93af2aa 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -1,11 +1,17 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; +import { setupServerAdmin } from "./setupServerAdmin"; +import logger from "@server/logger"; import { clearStaleData } from "./clearStaleData"; -import { ensureSetupToken } from "./ensureSetupToken"; export async function runSetupFunctions() { - await copyInConfig(); // copy in the config to the db as needed - await ensureActions(); // make sure all of the actions are in the db and the roles - await clearStaleData(); - await ensureSetupToken(); // ensure setup token exists for initial setup + try { + await copyInConfig(); // copy in the config to the db as needed + await setupServerAdmin(); + await ensureActions(); // make sure all of the actions are in the db and the roles + await clearStaleData(); + } catch (error) { + logger.error("Error running setup functions:", error); + process.exit(1); + } } diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrations.ts similarity index 75% rename from server/setup/migrationsSqlite.ts rename to server/setup/migrations.ts index b987b833..753ed6a7 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrations.ts @@ -1,35 +1,26 @@ -#! /usr/bin/env node import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { db, exists } from "../db/sqlite"; +import db, { exists } from "@server/db"; import path from "path"; import semver from "semver"; -import { versionMigrations } from "../db/sqlite"; +import { versionMigrations } from "@server/db/schemas"; import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; import { SqliteError } from "better-sqlite3"; import fs from "fs"; -import m1 from "./scriptsSqlite/1.0.0-beta1"; -import m2 from "./scriptsSqlite/1.0.0-beta2"; -import m3 from "./scriptsSqlite/1.0.0-beta3"; -import m4 from "./scriptsSqlite/1.0.0-beta5"; -import m5 from "./scriptsSqlite/1.0.0-beta6"; -import m6 from "./scriptsSqlite/1.0.0-beta9"; -import m7 from "./scriptsSqlite/1.0.0-beta10"; -import m8 from "./scriptsSqlite/1.0.0-beta12"; -import m13 from "./scriptsSqlite/1.0.0-beta13"; -import m15 from "./scriptsSqlite/1.0.0-beta15"; -import m16 from "./scriptsSqlite/1.0.0"; -import m17 from "./scriptsSqlite/1.1.0"; -import m18 from "./scriptsSqlite/1.2.0"; -import m19 from "./scriptsSqlite/1.3.0"; -import m20 from "./scriptsSqlite/1.5.0"; -import m21 from "./scriptsSqlite/1.6.0"; -import m22 from "./scriptsSqlite/1.7.0"; -import m23 from "./scriptsSqlite/1.8.0"; -import m24 from "./scriptsSqlite/1.9.0"; -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"; +import m1 from "./scripts/1.0.0-beta1"; +import m2 from "./scripts/1.0.0-beta2"; +import m3 from "./scripts/1.0.0-beta3"; +import m4 from "./scripts/1.0.0-beta5"; +import m5 from "./scripts/1.0.0-beta6"; +import m6 from "./scripts/1.0.0-beta9"; +import m7 from "./scripts/1.0.0-beta10"; +import m8 from "./scripts/1.0.0-beta12"; +import m13 from "./scripts/1.0.0-beta13"; +import m15 from "./scripts/1.0.0-beta15"; +import m16 from "./scripts/1.0.0"; +import m17 from "./scripts/1.1.0"; +import m18 from "./scripts/1.2.0"; +import m19 from "./scripts/1.3.0"; +import { setHostMeta } from "./setHostMeta"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -49,16 +40,7 @@ const migrations = [ { version: "1.0.0", run: m16 }, { version: "1.1.0", run: m17 }, { version: "1.2.0", run: m18 }, - { version: "1.3.0", run: m19 }, - { version: "1.5.0", run: m20 }, - { version: "1.6.0", run: m21 }, - { version: "1.7.0", run: m22 }, - { version: "1.8.0", run: m23 }, - { version: "1.9.0", run: m24 }, - { version: "1.10.0", run: m25 }, - { version: "1.10.1", run: m26 }, - { version: "1.10.2", run: m27 }, - { version: "1.11.0", run: m28 }, + { version: "1.3.0", run: m19 } // Add new migrations here as they are created ] as const; @@ -91,10 +73,6 @@ function backupDb() { } export async function runMigrations() { - if (process.env.DISABLE_MIGRATIONS) { - console.log("Migrations are disabled. Skipping..."); - return; - } try { const appVersion = APP_VERSION; @@ -136,7 +114,7 @@ async function executeScripts() { const pendingMigrations = lastExecuted .map((m) => m) .sort((a, b) => semver.compare(b.version, a.version)); - const startVersion = pendingMigrations[0]?.version ?? APP_VERSION; + const startVersion = pendingMigrations[0]?.version ?? "0.0.0"; console.log(`Starting migrations from version ${startVersion}`); const migrationsToRun = migrations.filter((migration) => diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts deleted file mode 100644 index de3785f3..00000000 --- a/server/setup/migrationsPg.ts +++ /dev/null @@ -1,152 +0,0 @@ -#! /usr/bin/env node -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { db } from "../db/pg"; -import semver from "semver"; -import { versionMigrations } from "../db/pg"; -import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; -import path from "path"; -import m1 from "./scriptsPg/1.6.0"; -import m2 from "./scriptsPg/1.7.0"; -import m3 from "./scriptsPg/1.8.0"; -import m4 from "./scriptsPg/1.9.0"; -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 - -// Define the migration list with versions and their corresponding functions -const migrations = [ - { version: "1.6.0", run: m1 }, - { version: "1.7.0", run: m2 }, - { version: "1.8.0", run: m3 }, - { version: "1.9.0", run: m4 }, - { version: "1.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; - run: () => Promise; -}[]; - -await run(); - -async function run() { - // run the migrations - await runMigrations(); -} - -export async function runMigrations() { - if (process.env.DISABLE_MIGRATIONS) { - console.log("Migrations are disabled. Skipping..."); - return; - } - try { - const appVersion = APP_VERSION; - - // determine if the migrations table exists - const exists = await db - .select() - .from(versionMigrations) - .limit(1) - .execute() - .then((res) => res.length > 0) - .catch(() => false); - - if (exists) { - console.log("Migrations table exists, running scripts..."); - await executeScripts(); - } else { - console.log("Migrations table does not exist, creating it..."); - console.log("Running migrations..."); - try { - await migrate(db, { - migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build - }); - console.log("Migrations completed successfully."); - } catch (error) { - console.error("Error running migrations:", error); - } - - await db - .insert(versionMigrations) - .values({ - version: appVersion, - executedAt: Date.now() - }) - .execute(); - } - } catch (e) { - console.error("Error running migrations:", e); - await new Promise((resolve) => - setTimeout(resolve, 1000 * 60 * 60 * 24 * 1) - ); - } -} - -async function executeScripts() { - try { - // Get the last executed version from the database - const lastExecuted = await db.select().from(versionMigrations); - - // Filter and sort migrations - const pendingMigrations = lastExecuted - .map((m) => m) - .sort((a, b) => semver.compare(b.version, a.version)); - const startVersion = pendingMigrations[0]?.version ?? "0.0.0"; - console.log(`Starting migrations from version ${startVersion}`); - - const migrationsToRun = migrations.filter((migration) => - semver.gt(migration.version, startVersion) - ); - - console.log( - "Migrations to run:", - migrationsToRun.map((m) => m.version).join(", ") - ); - - // Run migrations in order - for (const migration of migrationsToRun) { - console.log(`Running migration ${migration.version}`); - - try { - await migration.run(); - - // Update version in database - await db - .insert(versionMigrations) - .values({ - version: migration.version, - executedAt: Date.now() - }) - .execute(); - - console.log( - `Successfully completed migration ${migration.version}` - ); - } catch (e) { - if ( - e instanceof Error && - typeof (e as any).code === "string" && - (e as any).code === "23505" - ) { - console.error("Migration has already run! Skipping..."); - continue; // or return, depending on context - } - - console.error( - `Failed to run migration ${migration.version}:`, - e - ); - throw e; - } - } - - console.log("All migrations completed successfully"); - } catch (error) { - console.error("Migration process failed:", error); - throw error; - } -} diff --git a/server/setup/scriptsSqlite/1.0.0-beta1.ts b/server/setup/scripts/1.0.0-beta1.ts similarity index 100% rename from server/setup/scriptsSqlite/1.0.0-beta1.ts rename to server/setup/scripts/1.0.0-beta1.ts diff --git a/server/setup/scriptsSqlite/1.0.0-beta10.ts b/server/setup/scripts/1.0.0-beta10.ts similarity index 94% rename from server/setup/scriptsSqlite/1.0.0-beta10.ts rename to server/setup/scripts/1.0.0-beta10.ts index 400cbc31..6fd5289b 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta10.ts +++ b/server/setup/scripts/1.0.0-beta10.ts @@ -23,8 +23,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); delete rawConfig.server.secure_cookies; diff --git a/server/setup/scriptsSqlite/1.0.0-beta12.ts b/server/setup/scripts/1.0.0-beta12.ts similarity index 94% rename from server/setup/scriptsSqlite/1.0.0-beta12.ts rename to server/setup/scripts/1.0.0-beta12.ts index 8c96e663..0632b5e1 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta12.ts +++ b/server/setup/scripts/1.0.0-beta12.ts @@ -1,4 +1,4 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { configFilePath1, configFilePath2 } from "@server/lib/consts"; import { sql } from "drizzle-orm"; import fs from "fs"; @@ -25,8 +25,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); if (!rawConfig.flags) { rawConfig.flags = {}; diff --git a/server/setup/scriptsSqlite/1.0.0-beta13.ts b/server/setup/scripts/1.0.0-beta13.ts similarity index 96% rename from server/setup/scriptsSqlite/1.0.0-beta13.ts rename to server/setup/scripts/1.0.0-beta13.ts index 9ced727f..48b68cec 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta13.ts +++ b/server/setup/scripts/1.0.0-beta13.ts @@ -1,4 +1,4 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { sql } from "drizzle-orm"; const version = "1.0.0-beta.13"; diff --git a/server/setup/scriptsSqlite/1.0.0-beta15.ts b/server/setup/scripts/1.0.0-beta15.ts similarity index 95% rename from server/setup/scriptsSqlite/1.0.0-beta15.ts rename to server/setup/scripts/1.0.0-beta15.ts index cf39fd8a..a087c5c6 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta15.ts +++ b/server/setup/scripts/1.0.0-beta15.ts @@ -1,9 +1,9 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { configFilePath1, configFilePath2 } from "@server/lib/consts"; import fs from "fs"; import yaml from "js-yaml"; import { sql } from "drizzle-orm"; -import { domains, orgDomains, resources } from "@server/db"; +import { domains, orgDomains, resources } from "@server/db/schemas"; const version = "1.0.0-beta.15"; @@ -30,8 +30,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); const baseDomain = rawConfig.app.base_domain; const certResolver = rawConfig.traefik.cert_resolver; diff --git a/server/setup/scriptsSqlite/1.0.0-beta2.ts b/server/setup/scripts/1.0.0-beta2.ts similarity index 96% rename from server/setup/scriptsSqlite/1.0.0-beta2.ts rename to server/setup/scripts/1.0.0-beta2.ts index 1241e9c5..f8aa9bc3 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta2.ts +++ b/server/setup/scripts/1.0.0-beta2.ts @@ -22,8 +22,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); // Validate the structure if (!rawConfig.app || !rawConfig.app.base_url) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta3.ts b/server/setup/scripts/1.0.0-beta3.ts similarity index 94% rename from server/setup/scriptsSqlite/1.0.0-beta3.ts rename to server/setup/scripts/1.0.0-beta3.ts index fccfeb88..3bbaae81 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta3.ts +++ b/server/setup/scripts/1.0.0-beta3.ts @@ -22,8 +22,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); // Validate the structure if (!rawConfig.gerbil) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta5.ts b/server/setup/scripts/1.0.0-beta5.ts similarity index 97% rename from server/setup/scriptsSqlite/1.0.0-beta5.ts rename to server/setup/scripts/1.0.0-beta5.ts index 1c49503c..f0555121 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta5.ts +++ b/server/setup/scripts/1.0.0-beta5.ts @@ -25,8 +25,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); // Validate the structure if (!rawConfig.server) { diff --git a/server/setup/scriptsSqlite/1.0.0-beta6.ts b/server/setup/scripts/1.0.0-beta6.ts similarity index 91% rename from server/setup/scriptsSqlite/1.0.0-beta6.ts rename to server/setup/scripts/1.0.0-beta6.ts index 89129678..4fcfb114 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta6.ts +++ b/server/setup/scripts/1.0.0-beta6.ts @@ -23,8 +23,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); // Validate the structure if (!rawConfig.server) { @@ -43,8 +44,8 @@ export default async function migration() { const updatedYaml = yaml.dump(rawConfig); fs.writeFileSync(filePath, updatedYaml, "utf8"); } catch (error) { - console.log("We were unable to add CORS to your config file. Please add it manually."); - console.error(error); + console.log("We were unable to add CORS to your config file. Please add it manually.") + console.error(error) } console.log("Done."); diff --git a/server/setup/scriptsSqlite/1.0.0-beta9.ts b/server/setup/scripts/1.0.0-beta9.ts similarity index 96% rename from server/setup/scriptsSqlite/1.0.0-beta9.ts rename to server/setup/scripts/1.0.0-beta9.ts index 350293dc..64f2beed 100644 --- a/server/setup/scriptsSqlite/1.0.0-beta9.ts +++ b/server/setup/scripts/1.0.0-beta9.ts @@ -1,4 +1,4 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { emailVerificationCodes, passwordResetTokens, @@ -8,7 +8,7 @@ import { targets, userInvites, users -} from "../../db/sqlite"; +} from "@server/db/schemas"; import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; import { eq, sql } from "drizzle-orm"; import fs from "fs"; @@ -58,8 +58,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); rawConfig.server.resource_session_request_param = "p_session_request"; @@ -77,7 +78,7 @@ export default async function migration() { fs.writeFileSync(filePath, updatedYaml, "utf8"); } catch (e) { console.log( - `Failed to add resource_session_request_param to config. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file` + `Failed to add resource_session_request_param to config. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config` ); trx.rollback(); return; @@ -121,7 +122,7 @@ export default async function migration() { const traefikFileContents = fs.readFileSync(traefikPath, "utf8"); const traefikConfig = yaml.load(traefikFileContents) as any; - const parsedConfig: any = schema.safeParse(traefikConfig); + let parsedConfig: any = schema.safeParse(traefikConfig); if (parsedConfig.success) { // Ensure websecure entrypoint exists @@ -178,7 +179,7 @@ export default async function migration() { const traefikFileContents = fs.readFileSync(traefikPath, "utf8"); const traefikConfig = yaml.load(traefikFileContents) as any; - const parsedConfig: any = schema.safeParse(traefikConfig); + let parsedConfig: any = schema.safeParse(traefikConfig); if (parsedConfig.success) { // delete permanent from redirect-to-https middleware diff --git a/server/setup/scriptsSqlite/1.0.0.ts b/server/setup/scripts/1.0.0.ts similarity index 100% rename from server/setup/scriptsSqlite/1.0.0.ts rename to server/setup/scripts/1.0.0.ts diff --git a/server/setup/scriptsSqlite/1.1.0.ts b/server/setup/scripts/1.1.0.ts similarity index 94% rename from server/setup/scriptsSqlite/1.1.0.ts rename to server/setup/scripts/1.1.0.ts index 4d121852..8bd2cd19 100644 --- a/server/setup/scriptsSqlite/1.1.0.ts +++ b/server/setup/scripts/1.1.0.ts @@ -1,4 +1,4 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { sql } from "drizzle-orm"; const version = "1.1.0"; diff --git a/server/setup/scriptsSqlite/1.2.0.ts b/server/setup/scripts/1.2.0.ts similarity index 94% rename from server/setup/scriptsSqlite/1.2.0.ts rename to server/setup/scripts/1.2.0.ts index d6008407..fdea9fab 100644 --- a/server/setup/scriptsSqlite/1.2.0.ts +++ b/server/setup/scripts/1.2.0.ts @@ -1,4 +1,4 @@ -import { db } from "../../db/sqlite"; +import db from "@server/db"; import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; import { sql } from "drizzle-orm"; import fs from "fs"; @@ -43,8 +43,9 @@ export default async function migration() { } // Read and parse the YAML file + let rawConfig: any; const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + rawConfig = yaml.load(fileContents); if (!rawConfig.flags) { rawConfig.flags = {}; @@ -62,7 +63,7 @@ export default async function migration() { console.log(`Added new config option: resource_access_token_headers`); } catch (e) { console.log( - `Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file` + `Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config` ); console.error(e); } diff --git a/server/setup/scriptsSqlite/1.3.0.ts b/server/setup/scripts/1.3.0.ts similarity index 99% rename from server/setup/scriptsSqlite/1.3.0.ts rename to server/setup/scripts/1.3.0.ts index a084d59f..a75dc207 100644 --- a/server/setup/scriptsSqlite/1.3.0.ts +++ b/server/setup/scripts/1.3.0.ts @@ -177,7 +177,7 @@ export default async function migration() { } const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; + let rawConfig: any = yaml.load(fileContents); if (!rawConfig.server.secret) { rawConfig.server.secret = generateIdFromEntropySize(32); diff --git a/server/setup/scriptsPg/1.10.0.ts b/server/setup/scriptsPg/1.10.0.ts deleted file mode 100644 index 3be2f697..00000000 --- a/server/setup/scriptsPg/1.10.0.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; -import { readFileSync } from "fs"; -import path, { join } from "path"; - -const version = "1.10.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - const resources = await db.execute(sql` - SELECT "resourceId" FROM "resources" - `); - - const siteResources = await db.execute(sql` - SELECT "siteResourceId" FROM "siteResources" - `); - - await db.execute(sql`BEGIN`); - - await db.execute( - sql`ALTER TABLE "exitNodes" ADD COLUMN "region" text;` - ); - - await db.execute( - sql`ALTER TABLE "idpOidcConfig" ADD COLUMN "variant" text DEFAULT 'oidc' NOT NULL;` - ); - - await db.execute( - sql`ALTER TABLE "resources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;` - ); - - await db.execute( - sql`ALTER TABLE "siteResources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;` - ); - - await db.execute( - sql`ALTER TABLE "userOrgs" ADD COLUMN "autoProvisioned" boolean DEFAULT false;` - ); - - await db.execute( - sql`ALTER TABLE "targets" ADD COLUMN "pathMatchType" text;` - ); - - await db.execute(sql`ALTER TABLE "targets" ADD COLUMN "path" text;`); - - await db.execute( - sql`ALTER TABLE "resources" ADD COLUMN "headers" text;` - ); - - const usedNiceIds: string[] = []; - - for (const resource of resources.rows) { - // Generate a unique name and ensure it's unique - let niceId = ""; - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - niceId = generateName(); - if (!usedNiceIds.includes(niceId)) { - usedNiceIds.push(niceId); - break; - } - loops++; - } - await db.execute(sql` - UPDATE "resources" SET "niceId" = ${niceId} WHERE "resourceId" = ${resource.resourceId} - `); - } - - for (const resource of siteResources.rows) { - // Generate a unique name and ensure it's unique - let niceId = ""; - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - niceId = generateName(); - if (!usedNiceIds.includes(niceId)) { - usedNiceIds.push(niceId); - break; - } - loops++; - } - await db.execute(sql` - UPDATE "siteResources" SET "niceId" = ${niceId} WHERE "siteResourceId" = ${resource.siteResourceId} - `); - } - - // Handle auto-provisioned users for identity providers - const autoProvisionIdps = await db.execute(sql` - SELECT "idpId" FROM "idp" WHERE "autoProvision" = true - `); - - for (const idp of autoProvisionIdps.rows) { - // Get all users with this identity provider - const usersWithIdp = await db.execute(sql` - SELECT "id" FROM "user" WHERE "idpId" = ${idp.idpId} - `); - - // Update userOrgs to set autoProvisioned to true for these users - for (const user of usersWithIdp.rows) { - await db.execute(sql` - UPDATE "userOrgs" SET "autoProvisioned" = true WHERE "userId" = ${user.id} - `); - } - } - - await db.execute(sql`COMMIT`); - console.log(`Migrated database`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Failed to migrate db:", e); - throw e; - } -} - -const dev = process.env.ENVIRONMENT !== "prod"; -let file; -if (!dev) { - file = join(__DIRNAME, "names.json"); -} else { - file = join("server/db/names.json"); -} -export const names = JSON.parse(readFileSync(file, "utf-8")); - -export function generateName(): string { - const name = ( - names.descriptors[ - Math.floor(Math.random() * names.descriptors.length) - ] + - "-" + - names.animals[Math.floor(Math.random() * names.animals.length)] - ) - .toLowerCase() - .replace(/\s/g, "-"); - - // clean out any non-alphanumeric characters except for dashes - return name.replace(/[^a-z0-9-]/g, ""); -} diff --git a/server/setup/scriptsPg/1.10.2.ts b/server/setup/scriptsPg/1.10.2.ts deleted file mode 100644 index e59901a5..00000000 --- a/server/setup/scriptsPg/1.10.2.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; - -const version = "1.10.2"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - const resources = await db.execute(sql` - SELECT * FROM "resources" - `); - - await db.execute(sql`BEGIN`); - - for (const resource of resources.rows) { - const headers = resource.headers as string | null; - if (headers && headers !== "") { - // lets convert it to json - // fist split at commas - const headersArray = headers - .split(",") - .map((header: string) => { - const [name, ...valueParts] = header.split(":"); - const value = valueParts.join(":").trim(); - return { name: name.trim(), value }; - }); - - await db.execute(sql` - UPDATE "resources" SET "headers" = ${JSON.stringify(headersArray)} WHERE "resourceId" = ${resource.resourceId} - `); - - console.log( - `Updated resource ${resource.resourceId} headers to JSON format` - ); - } - } - - await db.execute(sql`COMMIT`); - console.log(`Migrated database`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/setup/scriptsPg/1.11.0.ts b/server/setup/scriptsPg/1.11.0.ts deleted file mode 100644 index 13186b4f..00000000 --- a/server/setup/scriptsPg/1.11.0.ts +++ /dev/null @@ -1,392 +0,0 @@ -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.6.0.ts b/server/setup/scriptsPg/1.6.0.ts deleted file mode 100644 index 30c9c269..00000000 --- a/server/setup/scriptsPg/1.6.0.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { configFilePath1, configFilePath2 } from "@server/lib/consts"; -import { sql } from "drizzle-orm"; -import fs from "fs"; -import yaml from "js-yaml"; - -const version = "1.6.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - db.execute(sql`UPDATE 'user' SET email = LOWER(email);`); - db.execute(sql`UPDATE 'user' SET username = LOWER(username);`); - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to make all usernames and emails lowercase"); - console.log(e); - } - - try { - // Determine which config file exists - const filePaths = [configFilePath1, configFilePath2]; - let filePath = ""; - for (const path of filePaths) { - if (fs.existsSync(path)) { - filePath = path; - break; - } - } - - if (!filePath) { - throw new Error( - `No config file found (expected config.yml or config.yaml).` - ); - } - - // Read and parse the YAML file - const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; - - if (rawConfig.server?.trust_proxy) { - rawConfig.server.trust_proxy = 1; - } - - // Write the updated YAML back to the file - const updatedYaml = yaml.dump(rawConfig); - fs.writeFileSync(filePath, updatedYaml, "utf8"); - - console.log(`Set trust_proxy to 1 in config file`); - } catch (e) { - console.log(`Unable to migrate config file. Error: ${e}`); - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsPg/1.7.0.ts b/server/setup/scriptsPg/1.7.0.ts deleted file mode 100644 index 3cb799e0..00000000 --- a/server/setup/scriptsPg/1.7.0.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; - -const version = "1.7.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - await db.execute(sql` - BEGIN; - - CREATE TABLE "clientSites" ( - "clientId" integer NOT NULL, - "siteId" integer NOT NULL, - "isRelayed" boolean DEFAULT false NOT NULL - ); - - CREATE TABLE "clients" ( - "id" serial PRIMARY KEY NOT NULL, - "orgId" varchar NOT NULL, - "exitNode" integer, - "name" varchar NOT NULL, - "pubKey" varchar, - "subnet" varchar NOT NULL, - "bytesIn" integer, - "bytesOut" integer, - "lastBandwidthUpdate" varchar, - "lastPing" varchar, - "type" varchar NOT NULL, - "online" boolean DEFAULT false NOT NULL, - "endpoint" varchar, - "lastHolePunch" integer, - "maxConnections" integer - ); - - CREATE TABLE "clientSession" ( - "id" varchar PRIMARY KEY NOT NULL, - "olmId" varchar NOT NULL, - "expiresAt" integer NOT NULL - ); - - CREATE TABLE "olms" ( - "id" varchar PRIMARY KEY NOT NULL, - "secretHash" varchar NOT NULL, - "dateCreated" varchar NOT NULL, - "clientId" integer - ); - - CREATE TABLE "roleClients" ( - "roleId" integer NOT NULL, - "clientId" integer NOT NULL - ); - - CREATE TABLE "webauthnCredentials" ( - "credentialId" varchar PRIMARY KEY NOT NULL, - "userId" varchar NOT NULL, - "publicKey" varchar NOT NULL, - "signCount" integer NOT NULL, - "transports" varchar, - "name" varchar, - "lastUsed" varchar NOT NULL, - "dateCreated" varchar NOT NULL, - "securityKeyName" varchar - ); - - CREATE TABLE "userClients" ( - "userId" varchar NOT NULL, - "clientId" integer NOT NULL - ); - - CREATE TABLE "webauthnChallenge" ( - "sessionId" varchar PRIMARY KEY NOT NULL, - "challenge" varchar NOT NULL, - "securityKeyName" varchar, - "userId" varchar, - "expiresAt" bigint NOT NULL - ); - - ALTER TABLE "limits" DISABLE ROW LEVEL SECURITY; - DROP TABLE "limits" CASCADE; - ALTER TABLE "sites" ALTER COLUMN "subnet" DROP NOT NULL; - ALTER TABLE "sites" ALTER COLUMN "bytesIn" SET DEFAULT 0; - ALTER TABLE "sites" ALTER COLUMN "bytesOut" SET DEFAULT 0; - ALTER TABLE "domains" ADD COLUMN "type" varchar; - ALTER TABLE "domains" ADD COLUMN "verified" boolean DEFAULT false NOT NULL; - ALTER TABLE "domains" ADD COLUMN "failed" boolean DEFAULT false NOT NULL; - ALTER TABLE "domains" ADD COLUMN "tries" integer DEFAULT 0 NOT NULL; - ALTER TABLE "exitNodes" ADD COLUMN "maxConnections" integer; - ALTER TABLE "newt" ADD COLUMN "version" varchar; - ALTER TABLE "orgs" ADD COLUMN "subnet" varchar; - ALTER TABLE "sites" ADD COLUMN "address" varchar; - ALTER TABLE "sites" ADD COLUMN "endpoint" varchar; - ALTER TABLE "sites" ADD COLUMN "publicKey" varchar; - ALTER TABLE "sites" ADD COLUMN "lastHolePunch" bigint; - ALTER TABLE "sites" ADD COLUMN "listenPort" integer; - ALTER TABLE "user" ADD COLUMN "twoFactorSetupRequested" boolean DEFAULT false; - ALTER TABLE "clientSites" ADD CONSTRAINT "clientSites_clientId_clients_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "clientSites" ADD CONSTRAINT "clientSites_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "clients" ADD CONSTRAINT "clients_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "clients" ADD CONSTRAINT "clients_exitNode_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNode") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE set null ON UPDATE no action; - ALTER TABLE "clientSession" ADD CONSTRAINT "clientSession_olmId_olms_id_fk" FOREIGN KEY ("olmId") REFERENCES "public"."olms"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "olms" ADD CONSTRAINT "olms_clientId_clients_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_clientId_clients_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "webauthnCredentials" ADD CONSTRAINT "webauthnCredentials_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "userClients" ADD CONSTRAINT "userClients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "webauthnChallenge" ADD CONSTRAINT "webauthnChallenge_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; - ALTER TABLE "resources" DROP COLUMN "isBaseDomain"; - - COMMIT; - `); - - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to migrate database schema"); - console.log(e); - throw e; - } - - try { - await db.execute(sql`BEGIN`); - - // Update all existing orgs to have the default subnet - await db.execute(sql`UPDATE "orgs" SET "subnet" = '100.90.128.0/24'`); - - // Get all orgs and their sites to assign sequential IP addresses - const orgsQuery = await db.execute(sql`SELECT "orgId" FROM "orgs"`); - - const orgs = orgsQuery.rows as { orgId: string }[]; - - for (const org of orgs) { - const sitesQuery = await db.execute(sql` - SELECT "siteId" FROM "sites" - WHERE "orgId" = ${org.orgId} - ORDER BY "siteId" - `); - - const sites = sitesQuery.rows as { siteId: number }[]; - - let ipIndex = 1; - for (const site of sites) { - const address = `100.90.128.${ipIndex}/24`; - await db.execute(sql` - UPDATE "sites" SET "address" = ${address} - WHERE "siteId" = ${site.siteId} - `); - ipIndex++; - } - } - - await db.execute(sql`COMMIT`); - console.log(`Updated org subnets and site addresses`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Unable to update org subnets"); - 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 deleted file mode 100644 index f3b6c613..00000000 --- a/server/setup/scriptsPg/1.8.0.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; - -const version = "1.8.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - await db.execute(sql` - BEGIN; - - ALTER TABLE "clients" ALTER COLUMN "bytesIn" SET DATA TYPE real; - ALTER TABLE "clients" ALTER COLUMN "bytesOut" SET DATA TYPE real; - ALTER TABLE "clientSession" ALTER COLUMN "expiresAt" SET DATA TYPE bigint; - ALTER TABLE "resources" ADD COLUMN "enableProxy" boolean DEFAULT true; - ALTER TABLE "sites" ADD COLUMN "remoteSubnets" text; - ALTER TABLE "user" ADD COLUMN "termsAcceptedTimestamp" varchar; - ALTER TABLE "user" ADD COLUMN "termsVersion" varchar; - - COMMIT; - `); - - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to migrate database schema"); - console.log(e); - throw e; - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts deleted file mode 100644 index fdbf3ae9..00000000 --- a/server/setup/scriptsPg/1.9.0.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; - -const version = "1.9.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const resourceSiteMap = new Map(); - let firstSiteId: number = 1; - - try { - // Get the first siteId to use as default - const firstSite = await db.execute(sql`SELECT "siteId" FROM "sites" LIMIT 1`); - if (firstSite.rows.length > 0) { - firstSiteId = firstSite.rows[0].siteId as number; - } - - const resources = await db.execute(sql` - SELECT "resourceId", "siteId" FROM "resources" WHERE "siteId" IS NOT NULL - `); - for (const resource of resources.rows) { - resourceSiteMap.set( - resource.resourceId as number, - resource.siteId as number - ); - } - } catch (e) { - console.log("Error getting resources:", e); - } - - try { - await db.execute(sql`BEGIN`); - - await db.execute(sql`CREATE TABLE "setupTokens" ( - "tokenId" varchar PRIMARY KEY NOT NULL, - "token" varchar NOT NULL, - "used" boolean DEFAULT false NOT NULL, - "dateCreated" varchar NOT NULL, - "dateUsed" varchar -);`); - - await db.execute(sql`CREATE TABLE "siteResources" ( - "siteResourceId" serial PRIMARY KEY NOT NULL, - "siteId" integer NOT NULL, - "orgId" varchar NOT NULL, - "name" varchar NOT NULL, - "protocol" varchar NOT NULL, - "proxyPort" integer NOT NULL, - "destinationPort" integer NOT NULL, - "destinationIp" varchar NOT NULL, - "enabled" boolean DEFAULT true NOT NULL -);`); - - await db.execute(sql`ALTER TABLE "resources" DROP CONSTRAINT "resources_siteId_sites_siteId_fk";`); - - await db.execute(sql`ALTER TABLE "clients" ALTER COLUMN "lastPing" TYPE integer USING NULL;`); - - await db.execute(sql`ALTER TABLE "clientSites" ADD COLUMN "endpoint" varchar;`); - - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "online" boolean DEFAULT false NOT NULL;`); - - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "lastPing" integer;`); - - await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "type" text DEFAULT 'gerbil';`); - - await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "version" text;`); - - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "createdAt" text;`); - - await db.execute(sql`ALTER TABLE "resources" ADD COLUMN "skipToIdpId" integer;`); - - await db.execute(sql.raw(`ALTER TABLE "targets" ADD COLUMN "siteId" integer NOT NULL DEFAULT ${firstSiteId || 1};`)); - - await db.execute(sql`ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`); - - await db.execute(sql`ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); - - await db.execute(sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_skipToIdpId_idp_idpId_fk" FOREIGN KEY ("skipToIdpId") REFERENCES "public"."idp"("idpId") ON DELETE cascade ON UPDATE no action;`); - - await db.execute(sql`ALTER TABLE "targets" ADD CONSTRAINT "targets_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`); - - await db.execute(sql`ALTER TABLE "clients" DROP COLUMN "endpoint";`); - - await db.execute(sql`ALTER TABLE "resources" DROP COLUMN "siteId";`); - - // for each resource, get all of its targets, and update the siteId to be the previously stored siteId - for (const [resourceId, siteId] of resourceSiteMap) { - const targets = await db.execute(sql` - SELECT "targetId" FROM "targets" WHERE "resourceId" = ${resourceId} - `); - for (const target of targets.rows) { - await db.execute(sql` - UPDATE "targets" SET "siteId" = ${siteId} WHERE "targetId" = ${target.targetId} - `); - } - } - - // list resources that have enableProxy false - // move them to the siteResources table - // remove them from the resources table - const proxyFalseResources = await db.execute(sql` - SELECT * FROM "resources" WHERE "enableProxy" = false - `); - - for (const resource of proxyFalseResources.rows) { - // Get the first target to derive destination IP and port - const firstTarget = await db.execute(sql` - SELECT "ip", "port" FROM "targets" WHERE "resourceId" = ${resource.resourceId} LIMIT 1 - `); - - if (firstTarget.rows.length === 0) { - continue; - } - - const target = firstTarget.rows[0]; - - // Insert into siteResources table - await db.execute(sql` - INSERT INTO "siteResources" ("siteId", "orgId", "name", "protocol", "proxyPort", "destinationPort", "destinationIp", "enabled") - VALUES (${resourceSiteMap.get(resource.resourceId as number)}, ${resource.orgId}, ${resource.name}, ${resource.protocol}, ${resource.proxyPort}, ${target.port}, ${target.ip}, ${resource.enabled}) - `); - - // Delete from resources table - await db.execute(sql` - DELETE FROM "resources" WHERE "resourceId" = ${resource.resourceId} - `); - - // Delete the targets for this resource - await db.execute(sql` - DELETE FROM "targets" WHERE "resourceId" = ${resource.resourceId} - `); - } - - await db.execute(sql`COMMIT`); - console.log(`Migrated database`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/setup/scriptsSqlite/1.10.0.ts b/server/setup/scriptsSqlite/1.10.0.ts deleted file mode 100644 index 3065a664..00000000 --- a/server/setup/scriptsSqlite/1.10.0.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import { readFileSync } from "fs"; -import path, { join } from "path"; - -const version = "1.10.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - try { - const resources = db - .prepare( - "SELECT resourceId FROM resources" - ) - .all() as Array<{ resourceId: number }>; - - const siteResources = db - .prepare( - "SELECT siteResourceId FROM siteResources" - ) - .all() as Array<{ siteResourceId: number }>; - - db.transaction(() => { - db.exec(` - ALTER TABLE 'exitNodes' ADD 'region' text; - ALTER TABLE 'idpOidcConfig' ADD 'variant' text DEFAULT 'oidc' NOT NULL; - ALTER TABLE 'resources' ADD 'niceId' text DEFAULT '' NOT NULL; - ALTER TABLE 'siteResources' ADD 'niceId' text DEFAULT '' NOT NULL; - ALTER TABLE 'userOrgs' ADD 'autoProvisioned' integer DEFAULT false; - ALTER TABLE 'targets' ADD 'pathMatchType' text; - ALTER TABLE 'targets' ADD 'path' text; - ALTER TABLE 'resources' ADD 'headers' text; - `); // this diverges from the schema a bit because the schema does not have a default on niceId but was required for the migration and I dont think it will effect much down the line... - - const usedNiceIds: string[] = []; - - for (const resourceId of resources) { - // Generate a unique name and ensure it's unique - let niceId = ""; - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - niceId = generateName(); - if (!usedNiceIds.includes(niceId)) { - usedNiceIds.push(niceId); - break; - } - loops++; - } - db.prepare( - `UPDATE resources SET niceId = ? WHERE resourceId = ?` - ).run(niceId, resourceId.resourceId); - } - - for (const resourceId of siteResources) { - // Generate a unique name and ensure it's unique - let niceId = ""; - let loops = 0; - while (true) { - if (loops > 100) { - throw new Error("Could not generate a unique name"); - } - - niceId = generateName(); - if (!usedNiceIds.includes(niceId)) { - usedNiceIds.push(niceId); - break; - } - loops++; - } - db.prepare( - `UPDATE siteResources SET niceId = ? WHERE siteResourceId = ?` - ).run(niceId, resourceId.siteResourceId); - } - - // Handle auto-provisioned users for identity providers - const autoProvisionIdps = db - .prepare( - "SELECT idpId FROM idp WHERE autoProvision = 1" - ) - .all() as Array<{ idpId: number }>; - - for (const idp of autoProvisionIdps) { - // Get all users with this identity provider - const usersWithIdp = db - .prepare( - "SELECT id FROM user WHERE idpId = ?" - ) - .all(idp.idpId) as Array<{ id: string }>; - - // Update userOrgs to set autoProvisioned to true for these users - for (const user of usersWithIdp) { - db.prepare( - "UPDATE userOrgs SET autoProvisioned = 1 WHERE userId = ?" - ).run(user.id); - } - } - })(); - - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} - -const dev = process.env.ENVIRONMENT !== "prod"; -let file; -if (!dev) { - file = join(__DIRNAME, "names.json"); -} else { - file = join("server/db/names.json"); -} -export const names = JSON.parse(readFileSync(file, "utf-8")); - -export function generateName(): string { - const name = ( - names.descriptors[ - Math.floor(Math.random() * names.descriptors.length) - ] + - "-" + - names.animals[Math.floor(Math.random() * names.animals.length)] - ) - .toLowerCase() - .replace(/\s/g, "-"); - - // clean out any non-alphanumeric characters except for dashes - return name.replace(/[^a-z0-9-]/g, ""); -} diff --git a/server/setup/scriptsSqlite/1.10.1.ts b/server/setup/scriptsSqlite/1.10.1.ts deleted file mode 100644 index 3608e92e..00000000 --- a/server/setup/scriptsSqlite/1.10.1.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.10.1"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - try { - db.pragma("foreign_keys = OFF"); - - db.transaction(() => { - db.exec(`ALTER TABLE "targets" RENAME TO "targets_old"; ---> statement-breakpoint -CREATE TABLE "targets" ( - "targetId" INTEGER PRIMARY KEY AUTOINCREMENT, - "resourceId" INTEGER NOT NULL, - "siteId" INTEGER NOT NULL, - "ip" TEXT NOT NULL, - "method" TEXT, - "port" INTEGER NOT NULL, - "internalPort" INTEGER, - "enabled" INTEGER NOT NULL DEFAULT 1, - "path" TEXT, - "pathMatchType" TEXT, - FOREIGN KEY ("resourceId") REFERENCES "resources"("resourceId") ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ("siteId") REFERENCES "sites"("siteId") ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -INSERT INTO "targets" ( - "targetId", - "resourceId", - "siteId", - "ip", - "method", - "port", - "internalPort", - "enabled", - "path", - "pathMatchType" -) -SELECT - targetId, - resourceId, - siteId, - ip, - method, - port, - internalPort, - enabled, - path, - pathMatchType -FROM "targets_old"; ---> statement-breakpoint -DROP TABLE "targets_old";`); - })(); - - db.pragma("foreign_keys = ON"); - - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/setup/scriptsSqlite/1.10.2.ts b/server/setup/scriptsSqlite/1.10.2.ts deleted file mode 100644 index 7978e262..00000000 --- a/server/setup/scriptsSqlite/1.10.2.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.10.2"; - -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); - - const resources = db.prepare("SELECT * FROM resources").all() as Array<{ - resourceId: number; - headers: string | null; - }>; - - try { - db.pragma("foreign_keys = OFF"); - - db.transaction(() => { - for (const resource of resources) { - const headers = resource.headers; - if (headers && headers !== "") { - // lets convert it to json - // fist split at commas - const headersArray = headers - .split(",") - .map((header: string) => { - const [name, ...valueParts] = header.split(":"); - const value = valueParts.join(":").trim(); - return { name: name.trim(), value }; - }); - - db.prepare( - ` - UPDATE "resources" SET "headers" = ? WHERE "resourceId" = ?` - ).run(JSON.stringify(headersArray), resource.resourceId); - - console.log( - `Updated resource ${resource.resourceId} headers to JSON format` - ); - } - } - })(); - - db.pragma("foreign_keys = ON"); - - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/setup/scriptsSqlite/1.11.0.ts b/server/setup/scriptsSqlite/1.11.0.ts deleted file mode 100644 index 1247eee9..00000000 --- a/server/setup/scriptsSqlite/1.11.0.ts +++ /dev/null @@ -1,342 +0,0 @@ -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/server/setup/scriptsSqlite/1.5.0.ts b/server/setup/scriptsSqlite/1.5.0.ts deleted file mode 100644 index 46e9ccca..00000000 --- a/server/setup/scriptsSqlite/1.5.0.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Database from "better-sqlite3"; -import path from "path"; -import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; -import fs from "fs"; -import yaml from "js-yaml"; - -const version = "1.5.0"; -const location = path.join(APP_PATH, "db", "db.sqlite"); - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const db = new Database(location); - - try { - db.pragma("foreign_keys = OFF"); - db.transaction(() => { - db.exec(` - ALTER TABLE 'sites' ADD 'dockerSocketEnabled' integer DEFAULT true NOT NULL; - `); - })(); // <-- executes the transaction immediately - db.pragma("foreign_keys = ON"); - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to migrate database schema"); - throw e; - } - - try { - // Determine which config file exists - const filePaths = [configFilePath1, configFilePath2]; - let filePath = ""; - for (const path of filePaths) { - if (fs.existsSync(path)) { - filePath = path; - break; - } - } - - if (!filePath) { - throw new Error( - `No config file found (expected config.yml or config.yaml).` - ); - } - - // Read and parse the YAML file - const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; - - if (rawConfig.cors?.headers) { - const headers = JSON.parse( - JSON.stringify(rawConfig.cors.headers) - ); - rawConfig.cors.allowed_headers = headers; - delete rawConfig.cors.headers; - } - - // Write the updated YAML back to the file - const updatedYaml = yaml.dump(rawConfig); - fs.writeFileSync(filePath, updatedYaml, "utf8"); - - console.log(`Migrated CORS headers to allowed_headers`); - } catch (e) { - console.log( - `Unable to migrate config file. Error: ${e}` - ); - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsSqlite/1.6.0.ts b/server/setup/scriptsSqlite/1.6.0.ts deleted file mode 100644 index adab2697..00000000 --- a/server/setup/scriptsSqlite/1.6.0.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import fs from "fs"; -import yaml from "js-yaml"; -import path from "path"; - -const version = "1.6.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - try { - db.pragma("foreign_keys = OFF"); - db.transaction(() => { - db.exec(` - UPDATE 'user' SET email = LOWER(email); - UPDATE 'user' SET username = LOWER(username); - `); - })(); // <-- executes the transaction immediately - db.pragma("foreign_keys = ON"); - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to make all usernames and emails lowercase"); - console.log(e); - } - - try { - // Determine which config file exists - const filePaths = [configFilePath1, configFilePath2]; - let filePath = ""; - for (const path of filePaths) { - if (fs.existsSync(path)) { - filePath = path; - break; - } - } - - if (!filePath) { - throw new Error( - `No config file found (expected config.yml or config.yaml).` - ); - } - - // Read and parse the YAML file - const fileContents = fs.readFileSync(filePath, "utf8"); - const rawConfig = yaml.load(fileContents) as any; - - if (rawConfig.server?.trust_proxy) { - rawConfig.server.trust_proxy = 1; - } - - // Write the updated YAML back to the file - const updatedYaml = yaml.dump(rawConfig); - fs.writeFileSync(filePath, updatedYaml, "utf8"); - - console.log(`Set trust_proxy to 1 in config file`); - } catch (e) { - console.log(`Unable to migrate config file. Please do it manually. Error: ${e}`); - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsSqlite/1.7.0.ts b/server/setup/scriptsSqlite/1.7.0.ts deleted file mode 100644 index f173d12e..00000000 --- a/server/setup/scriptsSqlite/1.7.0.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.7.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - try { - db.pragma("foreign_keys = OFF"); - - db.transaction(() => { - db.exec(` - CREATE TABLE 'clientSites' ( - 'clientId' integer NOT NULL, - 'siteId' integer NOT NULL, - 'isRelayed' integer DEFAULT 0 NOT NULL, - FOREIGN KEY ('clientId') REFERENCES 'clients'('id') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'clients' ( - 'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL, - 'orgId' text NOT NULL, - 'exitNode' integer, - 'name' text NOT NULL, - 'pubKey' text, - 'subnet' text NOT NULL, - 'bytesIn' integer, - 'bytesOut' integer, - 'lastBandwidthUpdate' text, - 'lastPing' text, - 'type' text NOT NULL, - 'online' integer DEFAULT 0 NOT NULL, - 'endpoint' text, - 'lastHolePunch' integer, - FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('exitNode') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null - ); - - CREATE TABLE 'clientSession' ( - 'id' text PRIMARY KEY NOT NULL, - 'olmId' text NOT NULL, - 'expiresAt' integer NOT NULL, - FOREIGN KEY ('olmId') REFERENCES 'olms'('id') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'olms' ( - 'id' text PRIMARY KEY NOT NULL, - 'secretHash' text NOT NULL, - 'dateCreated' text NOT NULL, - 'clientId' integer, - FOREIGN KEY ('clientId') REFERENCES 'clients'('id') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'roleClients' ( - 'roleId' integer NOT NULL, - 'clientId' integer NOT NULL, - FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('clientId') REFERENCES 'clients'('id') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'webauthnCredentials' ( - 'credentialId' text PRIMARY KEY NOT NULL, - 'userId' text NOT NULL, - 'publicKey' text NOT NULL, - 'signCount' integer NOT NULL, - 'transports' text, - 'name' text, - 'lastUsed' text NOT NULL, - 'dateCreated' text NOT NULL, - FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'userClients' ( - 'userId' text NOT NULL, - 'clientId' integer NOT NULL, - FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('clientId') REFERENCES 'clients'('id') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'userDomains' ( - 'userId' text NOT NULL, - 'domainId' text NOT NULL, - FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade - ); - - CREATE TABLE 'webauthnChallenge' ( - 'sessionId' text PRIMARY KEY NOT NULL, - 'challenge' text NOT NULL, - 'securityKeyName' text, - 'userId' text, - 'expiresAt' integer NOT NULL, - FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade - ); - - `); - - db.exec(` - CREATE TABLE '__new_sites' ( - 'siteId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, - 'orgId' text NOT NULL, - 'niceId' text NOT NULL, - 'exitNode' integer, - 'name' text NOT NULL, - 'pubKey' text, - 'subnet' text, - 'bytesIn' integer DEFAULT 0, - 'bytesOut' integer DEFAULT 0, - 'lastBandwidthUpdate' text, - 'type' text NOT NULL, - 'online' integer DEFAULT 0 NOT NULL, - 'address' text, - 'endpoint' text, - 'publicKey' text, - 'lastHolePunch' integer, - 'listenPort' integer, - 'dockerSocketEnabled' integer DEFAULT 1 NOT NULL, - FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('exitNode') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null - ); - - INSERT INTO '__new_sites' ( - 'siteId', 'orgId', 'niceId', 'exitNode', 'name', 'pubKey', 'subnet', 'bytesIn', 'bytesOut', 'lastBandwidthUpdate', 'type', 'online', 'address', 'endpoint', 'publicKey', 'lastHolePunch', 'listenPort', 'dockerSocketEnabled' - ) - SELECT siteId, orgId, niceId, exitNode, name, pubKey, subnet, bytesIn, bytesOut, lastBandwidthUpdate, type, online, NULL, NULL, NULL, NULL, NULL, dockerSocketEnabled - FROM sites; - - DROP TABLE 'sites'; - ALTER TABLE '__new_sites' RENAME TO 'sites'; - `); - - db.exec(` - ALTER TABLE 'domains' ADD 'type' text; - ALTER TABLE 'domains' ADD 'verified' integer DEFAULT 0 NOT NULL; - ALTER TABLE 'domains' ADD 'failed' integer DEFAULT 0 NOT NULL; - ALTER TABLE 'domains' ADD 'tries' integer DEFAULT 0 NOT NULL; - ALTER TABLE 'exitNodes' ADD 'maxConnections' integer; - ALTER TABLE 'newt' ADD 'version' text; - ALTER TABLE 'orgs' ADD 'subnet' text; - ALTER TABLE 'user' ADD 'twoFactorSetupRequested' integer DEFAULT 0; - ALTER TABLE 'resources' DROP COLUMN 'isBaseDomain'; - `); - })(); - - db.pragma("foreign_keys = ON"); - - console.log(`Migrated database schema`); - } catch (e) { - console.log("Unable to migrate database schema"); - throw e; - } - - db.transaction(() => { - // Update all existing orgs to have the default subnet - db.exec(`UPDATE 'orgs' SET 'subnet' = '100.90.128.0/24'`); - - // Get all orgs and their sites to assign sequential IP addresses - const orgs = db.prepare(`SELECT orgId FROM 'orgs'`).all() as { - orgId: string; - }[]; - - for (const org of orgs) { - const sites = db - .prepare( - `SELECT siteId FROM 'sites' WHERE orgId = ? ORDER BY siteId` - ) - .all(org.orgId) as { siteId: number }[]; - - let ipIndex = 1; - for (const site of sites) { - const address = `100.90.128.${ipIndex}/24`; - db.prepare( - `UPDATE 'sites' SET 'address' = ? WHERE siteId = ?` - ).run(address, site.siteId); - ipIndex++; - } - } - })(); - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsSqlite/1.8.0.ts b/server/setup/scriptsSqlite/1.8.0.ts deleted file mode 100644 index f8ac7c95..00000000 --- a/server/setup/scriptsSqlite/1.8.0.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.8.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - try { - db.transaction(() => { - db.exec(` - ALTER TABLE 'resources' ADD 'enableProxy' integer DEFAULT 1; - ALTER TABLE 'sites' ADD 'remoteSubnets' text; - ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text; - ALTER TABLE 'user' ADD 'termsVersion' text; - `); - })(); - - console.log("Migrated database schema"); - } catch (e) { - console.log("Unable to migrate database schema"); - throw e; - } - - console.log(`${version} migration complete`); -} diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts deleted file mode 100644 index 5f247ea5..00000000 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.9.0"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - const resourceSiteMap = new Map(); - let firstSiteId: number = 1; - - try { - // Get the first siteId to use as default - const firstSite = db.prepare("SELECT siteId FROM sites LIMIT 1").get() as { siteId: number } | undefined; - if (firstSite) { - firstSiteId = firstSite.siteId; - } - - const resources = db - .prepare( - "SELECT resourceId, siteId FROM resources WHERE siteId IS NOT NULL" - ) - .all() as Array<{ resourceId: number; siteId: number }>; - for (const resource of resources) { - resourceSiteMap.set(resource.resourceId, resource.siteId); - } - } catch (e) { - console.log("Error getting resources:", e); - } - - try { - db.pragma("foreign_keys = OFF"); - - db.transaction(() => { - db.exec(`CREATE TABLE 'setupTokens' ( - 'tokenId' text PRIMARY KEY NOT NULL, - 'token' text NOT NULL, - 'used' integer DEFAULT false NOT NULL, - 'dateCreated' text NOT NULL, - 'dateUsed' text -); ---> statement-breakpoint -CREATE TABLE 'siteResources' ( - 'siteResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, - 'siteId' integer NOT NULL, - 'orgId' text NOT NULL, - 'name' text NOT NULL, - 'protocol' text NOT NULL, - 'proxyPort' integer NOT NULL, - 'destinationPort' integer NOT NULL, - 'destinationIp' text NOT NULL, - 'enabled' integer DEFAULT true NOT NULL, - FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE '__new_resources' ( - 'resourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, - 'orgId' text NOT NULL, - 'name' text NOT NULL, - 'subdomain' text, - 'fullDomain' text, - 'domainId' text, - 'ssl' integer DEFAULT false NOT NULL, - 'blockAccess' integer DEFAULT false NOT NULL, - 'sso' integer DEFAULT true NOT NULL, - 'http' integer DEFAULT true NOT NULL, - 'protocol' text NOT NULL, - 'proxyPort' integer, - 'emailWhitelistEnabled' integer DEFAULT false NOT NULL, - 'applyRules' integer DEFAULT false NOT NULL, - 'enabled' integer DEFAULT true NOT NULL, - 'stickySession' integer DEFAULT false NOT NULL, - 'tlsServerName' text, - 'setHostHeader' text, - 'enableProxy' integer DEFAULT true, - 'skipToIdpId' integer, - FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null, - FOREIGN KEY ('skipToIdpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -INSERT INTO '__new_resources'("resourceId", "orgId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId") SELECT "resourceId", "orgId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", null FROM 'resources';--> statement-breakpoint -DROP TABLE 'resources';--> statement-breakpoint -ALTER TABLE '__new_resources' RENAME TO 'resources';--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE TABLE '__new_clients' ( - 'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL, - 'orgId' text NOT NULL, - 'exitNode' integer, - 'name' text NOT NULL, - 'pubKey' text, - 'subnet' text NOT NULL, - 'bytesIn' integer, - 'bytesOut' integer, - 'lastBandwidthUpdate' text, - 'lastPing' integer, - 'type' text NOT NULL, - 'online' integer DEFAULT false NOT NULL, - 'lastHolePunch' integer, - FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, - FOREIGN KEY ('exitNode') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint -INSERT INTO '__new_clients'("id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", "lastPing", "type", "online", "lastHolePunch") SELECT "id", "orgId", "exitNode", "name", "pubKey", "subnet", "bytesIn", "bytesOut", "lastBandwidthUpdate", NULL, "type", "online", "lastHolePunch" FROM 'clients';--> statement-breakpoint -DROP TABLE 'clients';--> statement-breakpoint -ALTER TABLE '__new_clients' RENAME TO 'clients';--> statement-breakpoint -ALTER TABLE 'clientSites' ADD 'endpoint' text;--> statement-breakpoint -ALTER TABLE 'exitNodes' ADD 'online' integer DEFAULT false NOT NULL;--> statement-breakpoint -ALTER TABLE 'exitNodes' ADD 'lastPing' integer;--> statement-breakpoint -ALTER TABLE 'exitNodes' ADD 'type' text DEFAULT 'gerbil';--> statement-breakpoint -ALTER TABLE 'olms' ADD 'version' text;--> statement-breakpoint -ALTER TABLE 'orgs' ADD 'createdAt' text;--> statement-breakpoint -ALTER TABLE 'targets' ADD 'siteId' integer NOT NULL DEFAULT ${firstSiteId || 1} REFERENCES sites(siteId);`); - - // for each resource, get all of its targets, and update the siteId to be the previously stored siteId - for (const [resourceId, siteId] of resourceSiteMap) { - const targets = db - .prepare( - "SELECT targetId FROM targets WHERE resourceId = ?" - ) - .all(resourceId) as Array<{ targetId: number }>; - for (const target of targets) { - db.prepare( - "UPDATE targets SET siteId = ? WHERE targetId = ?" - ).run(siteId, target.targetId); - } - } - - // list resources that have enableProxy false - // move them to the siteResources table - // remove them from the resources table - const proxyFalseResources = db - .prepare("SELECT * FROM resources WHERE enableProxy = 0") - .all() as Array; - - for (const resource of proxyFalseResources) { - // Get the first target to derive destination IP and port - const firstTarget = db - .prepare( - "SELECT ip, port FROM targets WHERE resourceId = ? LIMIT 1" - ) - .get(resource.resourceId) as - | { ip: string; port: number } - | undefined; - - if (!firstTarget) { - continue; - } - - // Insert into siteResources table - const stmt = db.prepare(` - INSERT INTO siteResources (siteId, orgId, name, protocol, proxyPort, destinationPort, destinationIp, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - stmt.run( - resourceSiteMap.get(resource.resourceId), - resource.orgId, - resource.name, - resource.protocol, - resource.proxyPort, - firstTarget.port, - firstTarget.ip, - resource.enabled - ); - - // Delete from resources table - db.prepare("DELETE FROM resources WHERE resourceId = ?").run( - resource.resourceId - ); - - // Delete the targets for this resource - db.prepare("DELETE FROM targets WHERE resourceId = ?").run( - resource.resourceId - ); - } - })(); - - db.pragma("foreign_keys = ON"); - - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/lib/hostMeta.ts b/server/setup/setHostMeta.ts similarity index 51% rename from server/lib/hostMeta.ts rename to server/setup/setHostMeta.ts index 2f2c7ed7..2a5b16a5 100644 --- a/server/lib/hostMeta.ts +++ b/server/setup/setHostMeta.ts @@ -1,9 +1,7 @@ -import { db, HostMeta } from "@server/db"; -import { hostMeta } from "@server/db"; +import db from "@server/db"; +import { hostMeta } from "@server/db/schemas"; import { v4 as uuidv4 } from "uuid"; -let gotHostMeta: HostMeta | undefined; - export async function setHostMeta() { const [existing] = await db.select().from(hostMeta).limit(1); @@ -17,12 +15,3 @@ export async function setHostMeta() { .insert(hostMeta) .values({ hostMetaId: id, createdAt: new Date().getTime() }); } - -export async function getHostMeta() { - if (gotHostMeta) { - return gotHostMeta; - } - const [meta] = await db.select().from(hostMeta).limit(1); - gotHostMeta = meta; - return meta; -} diff --git a/server/setup/setupServerAdmin.ts b/server/setup/setupServerAdmin.ts new file mode 100644 index 00000000..9a84852a --- /dev/null +++ b/server/setup/setupServerAdmin.ts @@ -0,0 +1,84 @@ +import { generateId, invalidateAllSessions } from "@server/auth/sessions/app"; +import { hashPassword, verifyPassword } from "@server/auth/password"; +import config from "@server/lib/config"; +import db from "@server/db"; +import { users } from "@server/db/schemas"; +import logger from "@server/logger"; +import { eq } from "drizzle-orm"; +import moment from "moment"; +import { fromError } from "zod-validation-error"; +import { passwordSchema } from "@server/auth/passwordSchema"; +import { UserType } from "@server/types/UserTypes"; + +export async function setupServerAdmin() { + const { + server_admin: { email, password } + } = config.getRawConfig().users; + + const parsed = passwordSchema.safeParse(password); + + if (!parsed.success) { + throw Error( + `Invalid server admin password: ${fromError(parsed.error).toString()}` + ); + } + + const passwordHash = await hashPassword(password); + + await db.transaction(async (trx) => { + try { + const [existing] = await trx + .select() + .from(users) + .where(eq(users.serverAdmin, true)); + + if (existing) { + const passwordChanged = !(await verifyPassword( + password, + existing.passwordHash! + )); + + if (passwordChanged) { + await trx + .update(users) + .set({ passwordHash }) + .where(eq(users.userId, existing.userId)); + + // this isn't using the transaction, but it's probably fine + await invalidateAllSessions(existing.userId); + + logger.info(`Server admin password updated`); + } + + if (existing.email !== email) { + await trx + .update(users) + .set({ email }) + .where(eq(users.userId, existing.userId)); + + logger.info(`Server admin email updated`); + } + } else { + const userId = generateId(15); + + await trx.update(users).set({ serverAdmin: false }); + + await db.insert(users).values({ + userId: userId, + email: email, + type: UserType.Internal, + username: email, + passwordHash, + dateCreated: moment().toISOString(), + serverAdmin: true, + emailVerified: true + }); + + logger.info(`Server admin created`); + } + } catch (e) { + logger.error(e); + trx.rollback(); + } + }); +} diff --git a/server/types/Auth.ts b/server/types/Auth.ts index 8e222987..ce86623f 100644 --- a/server/types/Auth.ts +++ b/server/types/Auth.ts @@ -1,6 +1,6 @@ import { Request } from "express"; -import { User } from "@server/db"; -import { Session } from "@server/db"; +import { User } from "@server/db/schemas"; +import { Session } from "@server/db/schemas"; export interface AuthenticatedRequest extends Request { user: User; diff --git a/src/actions/server.ts b/src/actions/server.ts deleted file mode 100644 index b9dc6e55..00000000 --- a/src/actions/server.ts +++ /dev/null @@ -1,417 +0,0 @@ -"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/components/OrganizationLandingCard.tsx b/src/app/[orgId]/OrganizationLandingCard.tsx similarity index 79% rename from src/components/OrganizationLandingCard.tsx rename to src/app/[orgId]/OrganizationLandingCard.tsx index f4d0d761..6bf0f57f 100644 --- a/src/components/OrganizationLandingCard.tsx +++ b/src/app/[orgId]/OrganizationLandingCard.tsx @@ -10,16 +10,7 @@ import { CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { - Users, - Globe, - Database, - Cog, - Settings, - Waypoints, - Combine -} from "lucide-react"; -import { useTranslations } from "next-intl"; +import { Users, Settings, Waypoints, Combine } from "lucide-react"; import { RoleItem } from "@server/routers/user"; interface OrgStat { @@ -48,21 +39,19 @@ export default function OrganizationLandingCard( ) { const [orgData] = useState(props); - const t = useTranslations(); - const orgStats: OrgStat[] = [ { - label: t("sites"), + label: "Sites", value: orgData.overview.stats.sites, icon: }, { - label: t("resources"), + label: "Resources", value: orgData.overview.stats.resources, icon: }, { - label: t("users"), + label: "Users", value: orgData.overview.stats.users, icon: } @@ -93,17 +82,21 @@ export default function OrganizationLandingCard( ))}
- {t("accessRoleYour", { - count: orgData.overview.isOwner - ? 1 - : orgData.overview.roles.length - })}{" "} + Your role + {orgData.overview.isOwner || + orgData.overview.isAdmin || + orgData.overview.roles.length === 1 + ? "" + : "s"} + :{" "} {orgData.overview.isOwner - ? t("accessRoleOwner") - : orgData.overview.roles - .map((r) => r.name) - .join(", ")} + ? "Owner" + : orgData.overview.isAdmin + ? "Admin" + : orgData.overview.roles + .map((r) => r.name) + .join(", ")}
@@ -112,7 +105,7 @@ export default function OrganizationLandingCard( diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index 585c9b48..fa41beb2 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,12 +1,13 @@ 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"; import { redirect } from "next/navigation"; import { cache } from "react"; -import SetLastOrgCookie from "@app/components/SetLastOrgCookie"; export default async function OrgLayout(props: { children: React.ReactNode; @@ -49,6 +50,8 @@ export default async function OrgLayout(props: { } return ( - + <> + {props.children} + ); } diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 25b3de1f..a9d78849 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,17 +1,15 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; -import MemberResourcesPortal from "../../components/MemberResourcesPortal"; +import OrganizationLandingCard from "./OrganizationLandingCard"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { redirect } from "next/navigation"; import { Layout } from "@app/components/Layout"; +import { orgLangingNavItems, orgNavItems, rootNavItems } from "../navigation"; import { ListUserOrgsResponse } from "@server/routers/org"; -import { pullEnv } from "@app/lib/pullEnv"; -import EnvProvider from "@app/providers/EnvProvider"; -import { orgLangingNavItems } from "@app/app/navigation"; type OrgPageProps = { params: Promise<{ orgId: string }>; @@ -20,7 +18,6 @@ type OrgPageProps = { export default async function OrgPage(props: OrgPageProps) { const params = await props.params; const orgId = params.orgId; - const env = pullEnv(); const getUser = cache(verifySession); const user = await getUser(); @@ -29,6 +26,7 @@ export default async function OrgPage(props: OrgPageProps) { redirect("/"); } + let redirectToSettings = false; let overview: GetOrgOverviewResponse | undefined; try { const res = await internal.get>( @@ -36,14 +34,16 @@ export default async function OrgPage(props: OrgPageProps) { await authCookieHeader() ); overview = res.data.data; + + if (overview.isAdmin || overview.isOwner) { + redirectToSettings = true; + } } catch (e) {} - // If user is admin or owner, redirect to settings - if (overview?.isAdmin || overview?.isOwner) { + if (redirectToSettings) { redirect(`/${orgId}/settings`); } - // For non-admin users, show the member resources portal let orgs: ListUserOrgsResponse["orgs"] = []; try { const getOrgs = cache(async () => @@ -60,8 +60,25 @@ export default async function OrgPage(props: OrgPageProps) { return ( - - {overview && } + + {overview && ( +
+ +
+ )}
); diff --git a/src/components/AccessPageHeaderAndNav.tsx b/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx similarity index 75% rename from src/components/AccessPageHeaderAndNav.tsx rename to src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx index 47690dc6..a3053e7e 100644 --- a/src/components/AccessPageHeaderAndNav.tsx +++ b/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx @@ -2,7 +2,6 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { useTranslations } from "next-intl"; interface AccessPageHeaderAndNavProps { children: React.ReactNode; @@ -13,22 +12,20 @@ export default function AccessPageHeaderAndNav({ children, hasInvitations }: AccessPageHeaderAndNavProps) { - const t = useTranslations(); - const navItems = [ { - title: t('users'), + title: "Users", href: `/{orgId}/settings/access/users` }, { - title: t('roles'), + title: "Roles", href: `/{orgId}/settings/access/roles` } ]; if (hasInvitations) { navItems.push({ - title: t('invite'), + title: "Invitations", href: `/{orgId}/settings/access/invitations` }); } @@ -36,8 +33,8 @@ export default function AccessPageHeaderAndNav({ return ( <> diff --git a/src/components/InvitationsDataTable.tsx b/src/app/[orgId]/settings/access/invitations/InvitationsDataTable.tsx similarity index 55% rename from src/components/InvitationsDataTable.tsx rename to src/app/[orgId]/settings/access/invitations/InvitationsDataTable.tsx index d73ad2ca..e2154b2d 100644 --- a/src/components/InvitationsDataTable.tsx +++ b/src/app/[orgId]/settings/access/invitations/InvitationsDataTable.tsx @@ -4,34 +4,23 @@ 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[]; - onRefresh?: () => void; - isRefreshing?: boolean; } export function InvitationsDataTable({ columns, - data, - onRefresh, - isRefreshing + data }: DataTableProps) { - - const t = useTranslations(); - return ( ); } diff --git a/src/components/InvitationsTable.tsx b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx similarity index 52% rename from src/components/InvitationsTable.tsx rename to src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx index 900003d7..9618df14 100644 --- a/src/components/InvitationsTable.tsx +++ b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx @@ -9,17 +9,14 @@ import { } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { MoreHorizontal } from "lucide-react"; -import { InvitationsDataTable } from "@app/components/InvitationsDataTable"; +import { InvitationsDataTable } from "./InvitationsDataTable"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import RegenerateInvitationForm from "@app/components/RegenerateInvitationForm"; +import RegenerateInvitationForm from "./RegenerateInvitationForm"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; 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; @@ -42,94 +39,67 @@ export default function InvitationsTable({ const [selectedInvitation, setSelectedInvitation] = useState(null); - const t = useTranslations(); - 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[] = [ + { + id: "dots", + cell: ({ row }) => { + const invitation = row.original; + return ( + + + + + + { + setIsRegenerateModalOpen(true); + setSelectedInvitation(invitation); + }} + > + Regenerate Invitation + + { + setIsDeleteModalOpen(true); + setSelectedInvitation(invitation); + }} + > + + Remove Invitation + + + + + ); + } + }, { accessorKey: "email", - header: t("email") + header: "Email" }, { accessorKey: "expiresAt", - header: t("expiresAt"), + header: "Expires At", cell: ({ row }) => { const expiresAt = new Date(row.original.expiresAt); const isExpired = expiresAt < new Date(); return ( - {moment(expiresAt).format("lll")} + {expiresAt.toLocaleString()} ); } }, { accessorKey: "role", - header: t("role") - }, - { - id: "dots", - cell: ({ row }) => { - const invitation = row.original; - return ( -
- - - - - - { - setIsDeleteModalOpen(true); - setSelectedInvitation(invitation); - }} - > - - {t("inviteRemove")} - - - - - - -
- ); - } + header: "Role" } ]; @@ -142,18 +112,17 @@ export default function InvitationsTable({ .catch((e) => { toast({ variant: "destructive", - title: t("inviteRemoveError"), - description: t("inviteRemoveErrorDescription") + title: "Failed to remove invitation", + description: + "An error occurred while removing the invitation." }); }); if (res && res.status === 200) { toast({ variant: "default", - title: t("inviteRemoved"), - description: t("inviteRemovedDescription", { - email: selectedInvitation.email - }) + title: "Invitation removed", + description: `The invitation for ${selectedInvitation.email} has been removed.` }); setInvitations((prev) => @@ -177,18 +146,23 @@ export default function InvitationsTable({ dialog={

- {t("inviteQuestionRemove", { - email: selectedInvitation?.email || "" - })} + Are you sure you want to remove the invitation for{" "} + {selectedInvitation?.email}? +

+

+ Once removed, this invitation will no longer be + valid. You can always re-invite the user later. +

+

+ To confirm, please type the email address of the + invitation below.

-

{t("inviteMessageRemove")}

-

{t("inviteMessageConfirm")}

} - buttonText={t("inviteRemoveConfirm")} + buttonText="Confirm Remove Invitation" onConfirm={removeInvitation} string={selectedInvitation?.email ?? ""} - title={t("inviteRemove")} + title="Remove Invitation" /> - + ); } diff --git a/src/components/RegenerateInvitationForm.tsx b/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx similarity index 76% rename from src/components/RegenerateInvitationForm.tsx rename to src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx index 786d3ae7..a8acb791 100644 --- a/src/components/RegenerateInvitationForm.tsx +++ b/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx @@ -24,7 +24,6 @@ import { SelectValue } from "@app/components/ui/select"; import { Label } from "@app/components/ui/label"; -import { useTranslations } from "next-intl"; type RegenerateInvitationFormProps = { open: boolean; @@ -57,16 +56,14 @@ export default function RegenerateInvitationForm({ const api = createApiClient(useEnvContext()); const { org } = useOrgContext(); - const t = useTranslations(); - const validForOptions = [ - { hours: 24, name: t('day', {count: 1}) }, - { hours: 48, name: t('day', {count: 2}) }, - { hours: 72, name: t('day', {count: 3}) }, - { hours: 96, name: t('day', {count: 4}) }, - { hours: 120, name: t('day', {count: 5}) }, - { hours: 144, name: t('day', {count: 6}) }, - { hours: 168, name: t('day', {count: 7}) } + { hours: 24, name: "1 day" }, + { hours: 48, name: "2 days" }, + { hours: 72, name: "3 days" }, + { hours: 96, name: "4 days" }, + { hours: 120, name: "5 days" }, + { hours: 144, name: "6 days" }, + { hours: 168, name: "7 days" } ]; useEffect(() => { @@ -82,8 +79,9 @@ export default function RegenerateInvitationForm({ if (!org?.org.orgId) { toast({ variant: "destructive", - title: t('orgMissing'), - description: t('orgMissingMessage'), + title: "Organization ID Missing", + description: + "Unable to regenerate invitation without an organization ID.", duration: 5000 }); return; @@ -107,15 +105,15 @@ export default function RegenerateInvitationForm({ if (sendEmail) { toast({ variant: "default", - title: t('inviteRegenerated'), - description: t('inviteSent', {email: invitation.email}), + title: "Invitation Regenerated", + description: `A new invitation has been sent to ${invitation.email}.`, duration: 5000 }); } else { toast({ variant: "default", - title: t('inviteRegenerated'), - description: t('inviteGenerate', {email: invitation.email}), + title: "Invitation Regenerated", + description: `A new invitation has been generated for ${invitation.email}.`, duration: 5000 }); } @@ -132,22 +130,24 @@ export default function RegenerateInvitationForm({ if (error.response?.status === 409) { toast({ variant: "destructive", - title: t('inviteDuplicateError'), - description: t('inviteDuplicateErrorDescription'), + title: "Duplicate Invite", + description: "An invitation for this user already exists.", duration: 5000 }); } else if (error.response?.status === 429) { toast({ variant: "destructive", - title: t('inviteRateLimitError'), - description: t('inviteRateLimitErrorDescription'), + title: "Rate Limit Exceeded", + description: + "You have exceeded the limit of 3 regenerations per hour. Please try again later.", duration: 5000 }); } else { toast({ variant: "destructive", - title: t('inviteRegenerateError'), - description: t('inviteRegenerateErrorDescription'), + title: "Failed to Regenerate Invitation", + description: + "An error occurred while regenerating the invitation.", duration: 5000 }); } @@ -168,16 +168,18 @@ export default function RegenerateInvitationForm({ > - {t('inviteRegenerate')} + Regenerate Invitation - {t('inviteRegenerateDescription')} + Revoke previous invitation and create a new one {!inviteLink ? (

- {t('inviteQuestionRegenerate', {email: invitation?.email || ""})} + Are you sure you want to regenerate the + invitation for {invitation?.email}? This + will revoke the previous invitation.

@@ -147,7 +146,7 @@ export default function CreateRoleForm({ name="description" render={({ field }) => ( - {t('description')} + Description @@ -160,7 +159,7 @@ export default function CreateRoleForm({ - + diff --git a/src/components/DeleteRoleForm.tsx b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx similarity index 84% rename from src/components/DeleteRoleForm.tsx rename to src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx index 3516851b..80d97267 100644 --- a/src/components/DeleteRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx @@ -34,11 +34,10 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { RoleRow } from "@app/components/RolesTable"; +import { RoleRow } from "./RolesTable"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; type CreateRoleFormProps = { open: boolean; @@ -47,6 +46,10 @@ type CreateRoleFormProps = { afterDelete?: () => void; }; +const formSchema = z.object({ + newRoleId: z.string({ message: "New role is required" }) +}); + export default function DeleteRoleForm({ open, roleToDelete, @@ -54,17 +57,12 @@ export default function DeleteRoleForm({ afterDelete }: CreateRoleFormProps) { const { org } = useOrgContext(); - const t = useTranslations(); const [loading, setLoading] = useState(false); const [roles, setRoles] = useState([]); const api = createApiClient(useEnvContext()); - const formSchema = z.object({ - newRoleId: z.string({ message: t('accessRoleErrorNewRequired') }) - }); - useEffect(() => { async function fetchRoles() { const res = await api @@ -75,10 +73,10 @@ export default function DeleteRoleForm({ console.error(e); toast({ variant: "destructive", - title: t('accessRoleErrorFetch'), + title: "Failed to fetch roles", description: formatAxiosError( e, - t('accessRoleErrorFetchDescription') + "An error occurred while fetching the roles" ) }); }); @@ -114,10 +112,10 @@ export default function DeleteRoleForm({ .catch((e) => { toast({ variant: "destructive", - title: t('accessRoleErrorRemove'), + title: "Failed to remove role", description: formatAxiosError( e, - t('accessRoleErrorRemoveDescription') + "An error occurred while removing the role." ) }); }); @@ -125,8 +123,8 @@ export default function DeleteRoleForm({ if (res && res.status === 200) { toast({ variant: "default", - title: t('accessRoleRemoved'), - description: t('accessRoleRemovedDescription') + title: "Role removed", + description: "The role has been successfully removed." }); if (open) { @@ -153,18 +151,22 @@ export default function DeleteRoleForm({ > - {t('accessRoleRemove')} + Remove Role - {t('accessRoleRemoveDescription')} + Remove a role from the organization +

- {t('accessRoleQuestionRemove', {name: roleToDelete.name})} + You're about to delete the{" "} + {roleToDelete.name} role. You cannot + undo this action.

- {t('accessRoleRequiredRemove')} + Before deleting this role, please select a + new role to transfer existing members to.

@@ -178,7 +180,7 @@ export default function DeleteRoleForm({ name="newRoleId" render={({ field }) => ( - {t('role')} + Role - - - - )} - /> - - ( - - - {t( - "inviteValid" - )} - - - - - )} - /> - - ( - - - {t("role")} - - - - - )} - /> - - {env.email.emailEnabled && ( -
- - setSendEmail( - e as boolean - ) - } - /> - -
- )} - - - - - - ) : ( - - - - {t("userInvited")} - - - {sendEmail - ? t( - "inviteEmailSentDescription" - ) - : t("inviteSentDescription")} - - - -
-

- {t("inviteExpiresIn", { - days: expiresInDays - })} -

- -
-
-
- )} - - )} - - {selectedOption && - selectedOption !== "internal" && - dataLoaded && ( - {t("userSettings")} + User Information - {t("userSettingsDescription")} + Enter the details for the new user - {/* Google/Azure Form */} - {(() => { - const selectedUserOption = - userOptions.find( - (opt) => - opt.id === - selectedOption - ); - return ( - selectedUserOption?.variant === - "google" || - selectedUserOption?.variant === - "azure" - ); - })() && ( -
- + + ( + + + Email + + + + + + )} - className="space-y-4" - id="create-user-form" - > - ( - - - {t("email")} - - - - - - - )} - /> + /> - ( - - - {t( - "nameOptional" + {env.email.emailEnabled && ( +
+ + setSendEmail( + e as boolean + ) + } + /> + +
+ )} + + ( + + + Valid For + + + + + )} + /> + + ( + + + Role + + + + + - - - )} - /> + + {roles.map( + ( + role + ) => ( + + { + role.name + } + + ) + )} + + + +
+ )} + /> - ( - - - {t("role")} - - - - + {inviteLink && ( +
+ {sendEmail && ( +

+ An email has + been sent to the + user with the + access link + below. They must + access the link + to accept the + invitation. +

)} - /> - - - )} + {!sendEmail && ( +

+ The user has + been invited. + They must access + the link below + to accept the + invitation. +

+ )} +

+ The invite will + expire in{" "} + + {expiresInDays}{" "} + {expiresInDays === + 1 + ? "day" + : "days"} + + . +

+ +
+ )} + + +
+
+
+ + )} - {/* Generic OIDC Form */} - {(() => { - const selectedUserOption = - userOptions.find( - (opt) => - opt.id === - selectedOption - ); - return ( - selectedUserOption?.variant !== - "google" && - selectedUserOption?.variant !== - "azure" - ); - })() && ( -
+ {userType !== "internal" && dataLoaded && ( + <> + + + + Identity Provider + + + Select the identity provider for the + external user + + + + {idps.length === 0 ? ( +

+ No identity providers are + configured. Please configure an + identity provider before creating + external users. +

+ ) : ( + + ( + + ({ + id: idp.idpId.toString(), + title: idp.name, + description: + formatIdpType( + idp.type + ) + }) + )} + defaultValue={ + field.value + } + onChange={( + value + ) => { + field.onChange( + value + ); + const idp = + idps.find( + (idp) => + idp.idpId.toString() === + value + ); + setSelectedIdp( + idp || null + ); + }} + cols={3} + /> + + + )} + /> + + )} +
+
+ + {idps.length > 0 && ( + + + + User Information + + + Enter the details for the new user + + + + +
( - {t( - "username" - )} + Username

- {t( - "usernameUniq" - )} + This must + match the + unique + username + that exists + in the + selected + identity + provider.

@@ -857,15 +670,14 @@ export default function Page() { ( - {t( - "emailOptional" - )} + Email + (Optional) ( - {t( - "nameOptional" - )} + Name + (Optional) ( - {t("role")} + Role - - - - )} - /> - - -
-
-
- - - - - {t('apiKeysGeneralSettings')} - - - {t('apiKeysGeneralSettingsDescription')} - - - - - - - - )} - - {apiKey && ( - - - - {t('apiKeysList')} - - - - - - - {t('name')} - - - - - - - - {t('created')} - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - {t('apiKeysSave')} - - - {t('apiKeysSaveDescription')} - - - - {/*

*/} - {/* {t('apiKeysInfo')} */} - {/*

*/} - - - - {/*
*/} - {/* */} - {/* ( */} - {/* */} - {/*
*/} - {/* { */} - {/* copiedForm.setValue( */} - {/* "copied", */} - {/* e as boolean */} - {/* ); */} - {/* }} */} - {/* /> */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/* )} */} - {/* /> */} - {/* */} - {/* */} -
-
- )} - - -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx deleted file mode 100644 index ca526a7d..00000000 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable"; -import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; -import { getTranslations } from 'next-intl/server'; - -type ApiKeyPageProps = { - params: Promise<{ orgId: string }>; -}; - -export const dynamic = "force-dynamic"; - -export default async function ApiKeysPage(props: ApiKeyPageProps) { - const params = await props.params; - const t = await getTranslations(); - - let apiKeys: ListOrgApiKeysResponse["apiKeys"] = []; - try { - const res = await internal.get>( - `/org/${params.orgId}/api-keys`, - await authCookieHeader() - ); - apiKeys = res.data.data.apiKeys; - } catch (e) {} - - const rows: OrgApiKeyRow[] = apiKeys.map((key) => { - return { - name: key.name, - id: key.apiKeyId, - key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`, - createdAt: key.createdAt - }; - }); - - return ( - <> - - - - - ); -} diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx deleted file mode 100644 index c7171c8d..00000000 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ /dev/null @@ -1,227 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useClientContext } from "@app/hooks/useClientContext"; -import { useForm } from "react-hook-form"; -import { toast } from "@app/hooks/useToast"; -import { useRouter } from "next/navigation"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter -} from "@app/components/Settings"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useEffect, useState } from "react"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { AxiosResponse } from "axios"; -import { ListSitesResponse } from "@server/routers/site"; -import { useTranslations } from "next-intl"; - -const GeneralFormSchema = z.object({ - name: z.string().nonempty("Name is required"), - siteIds: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - -type GeneralFormValues = z.infer; - -export default function GeneralPage() { - const t = useTranslations(); - const { client, updateClient } = useClientContext(); - const api = createApiClient(useEnvContext()); - const [loading, setLoading] = useState(false); - const router = useRouter(); - const [sites, setSites] = useState([]); - const [clientSites, setClientSites] = useState([]); - const [activeSitesTagIndex, setActiveSitesTagIndex] = useState(null); - - const form = useForm({ - resolver: zodResolver(GeneralFormSchema), - defaultValues: { - name: client?.name, - siteIds: [] - }, - mode: "onChange" - }); - - // Fetch available sites and client's assigned sites - useEffect(() => { - const fetchSites = async () => { - try { - // Fetch all available sites - const res = await api.get>( - `/org/${client?.orgId}/sites/` - ); - - const availableSites = res.data.data.sites - .filter((s) => s.type === "newt" && s.subnet) - .map((site) => ({ - id: site.siteId.toString(), - text: site.name - })); - - setSites(availableSites); - - // Filter sites to only include those assigned to the client - const assignedSites = availableSites.filter((site) => - client?.siteIds?.includes(parseInt(site.id)) - ); - setClientSites(assignedSites); - // Set the default values for the form - form.setValue("siteIds", assignedSites); - } catch (e) { - toast({ - variant: "destructive", - title: "Failed to fetch sites", - description: formatAxiosError( - e, - "An error occurred while fetching sites." - ) - }); - } - }; - - if (client?.clientId) { - fetchSites(); - } - }, [client?.clientId, client?.orgId, api, form]); - - async function onSubmit(data: GeneralFormValues) { - setLoading(true); - - try { - await api.post(`/client/${client?.clientId}`, { - name: data.name, - siteIds: data.siteIds.map(site => parseInt(site.id)) - }); - - updateClient({ name: data.name }); - - toast({ - title: t("clientUpdated"), - description: t("clientUpdatedDescription") - }); - - router.refresh(); - } catch (e) { - toast({ - variant: "destructive", - title: t("clientUpdateFailed"), - description: formatAxiosError( - e, - t("clientUpdateError") - ) - }); - } finally { - setLoading(false); - } - } - - return ( - - - - - {t("generalSettings")} - - - {t("generalSettingsDescription")} - - - - - -
- - ( - - {t("name")} - - - - - - )} - /> - - ( - - {t("sites")} - { - form.setValue( - "siteIds", - newTags as [Tag, ...Tag[]] - ); - }} - enableAutocomplete={true} - autocompleteOptions={sites} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={true} - sortTags={true} - /> - - {t("sitesDescription")} - - - - )} - /> - - -
-
- - - - -
-
- ); -} \ No newline at end of file diff --git a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx deleted file mode 100644 index c9c9fd14..00000000 --- a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { GetClientResponse } from "@server/routers/client"; -import ClientInfoCard from "../../../../../components/ClientInfoCard"; -import ClientProvider from "@app/providers/ClientProvider"; -import { redirect } from "next/navigation"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; - -type SettingsLayoutProps = { - children: React.ReactNode; - params: Promise<{ clientId: number | string; orgId: string }>; -} - -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; - - const { children } = props; - - let client = null; - try { - const res = await internal.get>( - `/client/${params.clientId}`, - await authCookieHeader() - ); - client = res.data.data; - } catch (error) { - console.error("Error fetching client data:", error); - redirect(`/${params.orgId}/settings/clients`); - } - - const navItems = [ - { - title: "General", - href: `/{orgId}/settings/clients/{clientId}/general` - } - ]; - - return ( - <> - - - -
- - - {children} - -
-
- - ); -} diff --git a/src/app/[orgId]/settings/clients/[clientId]/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/page.tsx deleted file mode 100644 index cbe9fb97..00000000 --- a/src/app/[orgId]/settings/clients/[clientId]/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function ClientPage(props: { - params: Promise<{ orgId: string; clientId: number | string }>; -}) { - const params = await props.params; - redirect(`/${params.orgId}/settings/clients/${params.clientId}/general`); -} diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx deleted file mode 100644 index e5765aea..00000000 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ /dev/null @@ -1,733 +0,0 @@ -"use client"; - -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { StrategySelect } from "@app/components/StrategySelect"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import HeaderTitle from "@app/components/SettingsSectionTitle"; -import { z } from "zod"; -import { createElement, useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Input } from "@app/components/ui/input"; -import { InfoIcon, Terminal } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import CopyTextBox from "@app/components/CopyTextBox"; -import CopyToClipboard from "@app/components/CopyToClipboard"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import { - FaApple, - FaCubes, - FaDocker, - FaFreebsd, - FaWindows -} from "react-icons/fa"; -import { - SiNixos, - SiKubernetes -} from "react-icons/si"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { - CreateClientBody, - CreateClientResponse, - PickClientDefaultsResponse -} from "@server/routers/client"; -import { ListSitesResponse } from "@server/routers/site"; -import { toast } from "@app/hooks/useToast"; -import { AxiosResponse } from "axios"; -import { useParams, useRouter } from "next/navigation"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; - -import { useTranslations } from "next-intl"; - -type ClientType = "olm"; - -interface TunnelTypeOption { - id: ClientType; - title: string; - description: string; - disabled?: boolean; -} - -type Commands = { - mac: Record; - linux: Record; - windows: Record; -}; - -const platforms = ["linux", "mac", "windows"] as const; - -type Platform = (typeof platforms)[number]; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const { orgId } = useParams(); - const router = useRouter(); - const t = useTranslations(); - - const createClientFormSchema = z.object({ - name: z - .string() - .min(2, { message: t("nameMin", { len: 2 }) }) - .max(30, { message: t("nameMax", { len: 30 }) }), - method: z.enum(["olm"]), - siteIds: z - .array( - z.object({ - id: z.string(), - text: z.string() - }) - ) - .refine((val) => val.length > 0, { - message: t("siteRequired") - }), - subnet: z.string().ip().min(1, { - message: t("subnetRequired") - }) - }); - - type CreateClientFormValues = z.infer; - - const [tunnelTypes, setTunnelTypes] = useState< - ReadonlyArray - >([ - { - id: "olm", - title: t("olmTunnel"), - description: t("olmTunnelDescription"), - disabled: true - } - ]); - - const [loadingPage, setLoadingPage] = useState(true); - const [sites, setSites] = useState([]); - const [activeSitesTagIndex, setActiveSitesTagIndex] = useState< - number | null - >(null); - - const [platform, setPlatform] = useState("linux"); - const [architecture, setArchitecture] = useState("amd64"); - const [commands, setCommands] = useState(null); - - const [olmId, setOlmId] = useState(""); - const [olmSecret, setOlmSecret] = useState(""); - const [olmCommand, setOlmCommand] = useState(""); - - const [createLoading, setCreateLoading] = useState(false); - - const [clientDefaults, setClientDefaults] = - useState(null); - - const hydrateCommands = ( - id: string, - secret: string, - endpoint: string, - version: string - ) => { - const commands = { - mac: { - "Apple Silicon (arm64)": [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ], - "Intel x64 (amd64)": [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ] - }, - linux: { - amd64: [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ], - arm64: [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ], - arm32: [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ], - arm32v6: [ - `curl -fsSL https://digpangolin.com/get-olm.sh | bash`, - `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` - ], - riscv64: [ - `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"`, - `# Then run olm with your credentials`, - `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` - ] - } - }; - setCommands(commands); - }; - - const getArchitectures = () => { - switch (platform) { - case "linux": - return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; - case "mac": - return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; - case "windows": - return ["x64"]; - default: - return ["x64"]; - } - }; - - const getPlatformName = (platformName: string) => { - switch (platformName) { - case "windows": - return "Windows"; - case "mac": - return "macOS"; - case "docker": - return "Docker"; - default: - return "Linux"; - } - }; - - const getCommand = () => { - const placeholder = [t("unknownCommand")]; - if (!commands) { - return placeholder; - } - let platformCommands = commands[platform as keyof Commands]; - - if (!platformCommands) { - // get first key - const firstPlatform = Object.keys(commands)[0] as Platform; - platformCommands = commands[firstPlatform as keyof Commands]; - - setPlatform(firstPlatform); - } - - let architectureCommands = platformCommands[architecture]; - if (!architectureCommands) { - // get first key - const firstArchitecture = Object.keys(platformCommands)[0]; - architectureCommands = platformCommands[firstArchitecture]; - - setArchitecture(firstArchitecture); - } - - return architectureCommands || placeholder; - }; - - const getPlatformIcon = (platformName: string) => { - switch (platformName) { - case "windows": - return ; - case "mac": - return ; - case "docker": - return ; - case "kubernetes": - return ; - case "podman": - return ; - case "freebsd": - return ; - case "nixos": - return ; - default: - return ; - } - }; - - const form = useForm({ - resolver: zodResolver(createClientFormSchema), - defaultValues: { - name: "", - method: "olm", - siteIds: [], - subnet: "" - } - }); - - async function onSubmit(data: CreateClientFormValues) { - setCreateLoading(true); - - if (!clientDefaults) { - toast({ - variant: "destructive", - title: t("errorCreatingClient"), - description: t("clientDefaultsNotFound") - }); - setCreateLoading(false); - return; - } - - const payload: CreateClientBody = { - name: data.name, - type: data.method as "olm", - siteIds: data.siteIds.map((site) => parseInt(site.id)), - olmId: clientDefaults.olmId, - secret: clientDefaults.olmSecret, - subnet: data.subnet - }; - - const res = await api - .put< - AxiosResponse - >(`/org/${orgId}/client`, payload) - .catch((e) => { - toast({ - variant: "destructive", - title: t("errorCreatingClient"), - description: formatAxiosError(e) - }); - }); - - if (res && res.status === 201) { - const data = res.data.data; - router.push(`/${orgId}/settings/clients/${data.clientId}`); - } - - setCreateLoading(false); - } - - useEffect(() => { - const load = async () => { - setLoadingPage(true); - - // Fetch available sites - - const res = await api.get>( - `/org/${orgId}/sites/` - ); - const sites = res.data.data.sites.filter( - (s) => s.type === "newt" && s.subnet - ); - setSites( - sites.map((site) => ({ - id: site.siteId.toString(), - text: site.name - })) - ); - - let olmVersion = "latest"; - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3000); - - const response = await fetch( - `https://api.github.com/repos/fosrl/olm/releases/latest`, - { signal: controller.signal } - ); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error( - t("olmErrorFetchReleases", { - err: response.statusText - }) - ); - } - const data = await response.json(); - const latestVersion = data.tag_name; - olmVersion = latestVersion; - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - console.error(t("olmErrorFetchTimeout")); - } else { - console.error( - t("olmErrorFetchLatest", { - err: - error instanceof Error - ? error.message - : String(error) - }) - ); - } - } - - await api - .get(`/org/${orgId}/pick-client-defaults`) - .catch((e) => { - form.setValue("method", "olm"); - }) - .then((res) => { - if (res && res.status === 200) { - const data = res.data.data; - - setClientDefaults(data); - - const olmId = data.olmId; - const olmSecret = data.olmSecret; - const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`; - - setOlmId(olmId); - setOlmSecret(olmSecret); - setOlmCommand(olmCommand); - - hydrateCommands( - olmId, - olmSecret, - env.app.dashboardUrl, - olmVersion - ); - - if (data.subnet) { - form.setValue("subnet", data.subnet); - } - - setTunnelTypes((prev: any) => { - return prev.map((item: any) => { - return { ...item, disabled: false }; - }); - }); - } - }); - - setLoadingPage(false); - }; - - load(); - }, []); - - return ( - <> -
- - -
- - {!loadingPage && ( -
- - - - - {t("clientInformation")} - - - - -
- { - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} - className="space-y-4" - id="create-client-form" - > - ( - - - {t("name")} - - - - - - - )} - /> - - ( - - - {t("address")} - - - - - - - {t("addressDescription")} - - - )} - /> - - ( - - - {t("sites")} - - { - form.setValue( - "siteIds", - olmags as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - sites - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - {t("sitesDescription")} - - - - )} - /> - - -
-
-
- - {form.watch("method") === "olm" && ( - <> - - - - {t("clientOlmCredentials")} - - - {t("clientOlmCredentialsDescription")} - - - - - - - {t("olmEndpoint")} - - - - - - - - {t("olmId")} - - - - - - - - {t("olmSecretKey")} - - - - - - - - - - - {t("clientCredentialsSave")} - - - {t( - "clientCredentialsSaveDescription" - )} - - - - - - - - {t("clientInstallOlm")} - - - {t("clientInstallOlmDescription")} - - - -
-

- {t("operatingSystem")} -

-
- {platforms.map((os) => ( - - ))} -
-
- -
-

- {["docker", "podman"].includes( - platform - ) - ? t("method") - : t("architecture")} -

-
- {getArchitectures().map( - (arch) => ( - - ) - )} -
-
-

- {t("commands")} -

-
- -
-
-
-
-
- - )} -
- -
- - -
-
- )} - - ); -} diff --git a/src/app/[orgId]/settings/clients/layout.tsx b/src/app/[orgId]/settings/clients/layout.tsx deleted file mode 100644 index 59a46414..00000000 --- a/src/app/[orgId]/settings/clients/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { redirect } from "next/navigation"; -import { pullEnv } from "@app/lib/pullEnv"; - -export const dynamic = "force-dynamic"; - -interface SettingsLayoutProps { - children: React.ReactNode; - params: Promise<{ orgId: string }>; -} - -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; - const { children } = props; - const env = pullEnv(); - - if (!env.flags.enableClients) { - redirect(`/${params.orgId}/settings`); - } - - return children; -} diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx deleted file mode 100644 index 0813ad3c..00000000 --- a/src/app/[orgId]/settings/clients/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -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 }>; -}; - -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 { - const res = await internal.get>( - `/org/${params.orgId}/clients`, - await authCookieHeader() - ); - clients = res.data.data.clients; - } catch (e) {} - - function formatSize(mb: number): string { - if (mb >= 1024 * 1024) { - return `${(mb / (1024 * 1024)).toFixed(2)} TB`; - } else if (mb >= 1024) { - return `${(mb / 1024).toFixed(2)} GB`; - } else { - return `${mb.toFixed(2)} MB`; - } - } - - const clientRows: ClientRow[] = clients.map((client) => { - return { - name: client.name, - id: client.clientId, - subnet: client.subnet.split("/")[0], - mbIn: formatSize(client.megabytesIn || 0), - mbOut: formatSize(client.megabytesOut || 0), - orgId: params.orgId, - online: client.online - }; - }); - - return ( - <> - - - - - ); -} diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx deleted file mode 100644 index cb587d92..00000000 --- a/src/app/[orgId]/settings/domains/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import DomainsTable, { DomainRow } from "../../../../components/DomainsTable"; -import { getTranslations } from "next-intl/server"; -import { cache } from "react"; -import { GetOrgResponse } from "@server/routers/org"; -import { redirect } from "next/navigation"; -import OrgProvider from "@app/providers/OrgProvider"; -import { ListDomainsResponse } from "@server/routers/domain"; -import { toUnicode } from 'punycode'; - -type Props = { - params: Promise<{ orgId: string }>; -}; - -export default async function DomainsPage(props: Props) { - const params = await props.params; - - let domains: DomainRow[] = []; - try { - const res = await internal.get< - AxiosResponse - >(`/org/${params.orgId}/domains`, await authCookieHeader()); - - const rawDomains = res.data.data.domains as DomainRow[]; - - domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - })); - } catch (e) { - console.error(e); - } - - let org = null; - try { - const getOrg = cache(async () => - internal.get>( - `/org/${params.orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); - org = res.data.data; - } catch { - redirect(`/${params.orgId}`); - } - - if (!org) { - } - - const t = await getTranslations(); - - return ( - <> - - - - - - ); -} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 3fae9ce4..a2d9cc0a 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -10,7 +10,6 @@ import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; type GeneralSettingsProps = { children: React.ReactNode; @@ -58,11 +57,9 @@ export default async function GeneralSettingsPage({ redirect(`/${orgId}`); } - const t = await getTranslations(); - const navItems = [ { - title: t('general'), + title: "General", href: `/{orgId}/settings/general`, }, ]; @@ -72,8 +69,8 @@ export default async function GeneralSettingsPage({ diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 19352408..9819be59 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -1,14 +1,11 @@ "use client"; -import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import AuthPageSettings, { - AuthPageSettingsRef -} from "@app/components/private/AuthPageSettings"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; 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, useRef } from "react"; +import { useState } from "react"; import { Form, FormControl, @@ -19,16 +16,23 @@ 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"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { formatAxiosError } from "@app/lib/api"; +import { AlertTriangle, Trash2 } from "lucide-react"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle +} from "@/components/ui/card"; import { AxiosResponse } from "axios"; import { DeleteOrgResponse, ListUserOrgsResponse } from "@server/routers/org"; -import { useRouter } from "next/navigation"; +import { redirect, useRouter } from "next/navigation"; import { SettingsContainer, SettingsSection, @@ -40,35 +44,29 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; -import { useTranslations } from "next-intl"; -// Schema for general organization settings const GeneralFormSchema = z.object({ - name: z.string(), - subnet: z.string().optional() + name: z.string() }); type GeneralFormValues = z.infer; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const { orgUser } = userOrgUserContext(); const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); const { user } = useUserContext(); - const t = useTranslations(); - const { env } = useEnvContext(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); - const authPageSettingsRef = useRef(null); - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { - name: org?.org.name, - subnet: org?.org.subnet || "" // Add default value for subnet + name: org?.org.name }, mode: "onChange" }); @@ -79,10 +77,12 @@ export default function GeneralPage() { const res = await api.delete>( `/org/${org?.org.orgId}` ); + toast({ - title: t("orgDeleted"), - description: t("orgDeletedMessage") + title: "Organization deleted", + description: "The organization and its data has been deleted." }); + if (res.status === 200) { pickNewOrgAndNavigate(); } @@ -90,8 +90,11 @@ export default function GeneralPage() { console.error(err); toast({ variant: "destructive", - title: t("orgErrorDelete"), - description: formatAxiosError(err, t("orgErrorDeleteMessage")) + title: "Failed to delete org", + description: formatAxiosError( + err, + "An error occurred while deleting the org." + ) }); } finally { setLoadingDelete(false); @@ -118,44 +121,42 @@ export default function GeneralPage() { console.error(err); toast({ variant: "destructive", - title: t("orgErrorFetch"), - description: formatAxiosError(err, t("orgErrorFetchMessage")) + title: "Failed to fetch orgs", + description: formatAxiosError( + err, + "An error occurred while listing your orgs" + ) }); } } async function onSubmit(data: GeneralFormValues) { setLoadingSave(true); - - try { - // Update organization - await api.post(`/org/${org?.org.orgId}`, { + await api + .post(`/org/${org?.org.orgId}`, { name: data.name - // subnet: data.subnet // Include subnet in the API request - }); + }) + .then(() => { + toast({ + title: "Organization updated", + description: "The organization has been updated." + }); - // 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: "Failed to update org", + description: formatAxiosError( + e, + "An error occurred while updating the org." + ) + }); + }) + .finally(() => { + setLoadingSave(false); }); - router.refresh(); - } catch (e) { - toast({ - variant: "destructive", - title: t("orgErrorUpdate"), - description: formatAxiosError(e, t("orgErrorUpdateMessage")) - }); - } finally { - setLoadingSave(false); - } } return ( @@ -168,28 +169,34 @@ export default function GeneralPage() { dialog={

- {t("orgQuestionRemove", { - selectedOrg: org?.org.name - })} + Are you sure you want to delete the organization{" "} + {org?.org.name}? +

+

+ This action is irreversible and will delete all + associated data. +

+

+ To confirm, type the name of the organization below.

-

{t("orgMessageRemove")}

-

{t("orgMessageConfirm")}

} - buttonText={t("orgDeleteConfirm")} + buttonText="Confirm Delete Organization" onConfirm={deleteOrg} string={org?.org.name || ""} - title={t("orgDelete")} + title="Delete Organization" /> + - {t("orgGeneralSettings")} + Organization Settings - {t("orgGeneralSettingsDescription")} + Manage your organization details and configuration +
@@ -203,71 +210,44 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - {t("name")} + Name - {t("orgDisplayName")} + This is the display name of the + organization. )} /> - {env.flags.enableClients && ( - ( - - - {t("subnet")} - - - - - - - {t("subnetDescription")} - - - )} - /> - )}
+ + + +
- {/*(build === "saas") && ( - - )*/} - - {/* Save Button */} -
- -
- - - {t("orgDangerZone")} - + Danger Zone - {t("orgDangerZoneDescription")} + Once you delete this org, there is no going back. Please + be certain. + diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index d35af6e6..ac5e552b 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -1,18 +1,16 @@ import { Metadata } from "next"; import { + Cog, Combine, - KeyRound, LinkIcon, Settings, Users, - Waypoints, - Workflow + Waypoints } from "lucide-react"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; -import { ListOrgsResponse } from "@server/routers/org"; import { GetOrgResponse, ListUserOrgsResponse } from "@server/routers/org"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; @@ -20,14 +18,12 @@ import { GetOrgUserResponse } from "@server/routers/user"; import UserProvider from "@app/providers/UserProvider"; import { Layout } from "@app/components/Layout"; import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav"; -import { getTranslations } from "next-intl/server"; -import { pullEnv } from "@app/lib/pullEnv"; -import { orgNavSections } from "@app/app/navigation"; +import { orgNavItems } from "@app/app/navigation"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { - title: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, + title: `Settings - Pangolin`, description: "" }; @@ -44,16 +40,12 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const getUser = cache(verifySession); const user = await getUser(); - const env = pullEnv(); - if (!user) { redirect(`/`); } const cookie = await authCookieHeader(); - const t = await getTranslations(); - try { const getOrgUser = cache(() => internal.get>( @@ -64,7 +56,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const orgUser = await getOrgUser(); if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) { - throw new Error(t("userErrorNotAdminOrOwner")); + throw new Error("User is not an admin or owner"); } } catch { redirect(`/${params.orgId}`); @@ -86,7 +78,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( - + {children} diff --git a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx new file mode 100644 index 00000000..a9db3e79 --- /dev/null +++ b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { + ColumnDef, +} from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + createResource?: () => void; +} + +export function ResourcesDataTable({ + columns, + data, + createResource +}: DataTableProps) { + return ( + + ); +} diff --git a/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx b/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx new file mode 100644 index 00000000..d16a0a57 --- /dev/null +++ b/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports +import { Card, CardContent } from "@app/components/ui/card"; +import { Button } from "@app/components/ui/button"; + +export const ResourcesSplashCard = () => { + const [isDismissed, setIsDismissed] = useState(false); + + const key = "resources-splash-dismissed"; + + useEffect(() => { + const dismissed = localStorage.getItem(key); + if (dismissed === "true") { + setIsDismissed(true); + } + }, []); + + const handleDismiss = () => { + setIsDismissed(true); + localStorage.setItem(key, "true"); + }; + + if (isDismissed) { + return null; + } + + return ( + + + +
+

+ + Resources +

+

+ 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. +

+
    +
  • + + Secure connectivity with WireGuard encryption +
  • +
  • + + Configure multiple authentication methods +
  • +
  • + + User and role-based access control +
  • +
+
+
+
+ ); +}; + +export default ResourcesSplashCard; diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx new file mode 100644 index 00000000..bfb4f08b --- /dev/null +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ResourcesDataTable } from "./ResourcesDataTable"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { + Copy, + ArrowRight, + ArrowUpDown, + MoreHorizontal, + Check, + ArrowUpRight, + ShieldOff, + ShieldCheck +} from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { formatAxiosError } from "@app/lib/api"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { Switch } from "@app/components/ui/switch"; +import { AxiosResponse } from "axios"; +import { UpdateResourceResponse } from "@server/routers/resource"; + +export type ResourceRow = { + id: number; + name: string; + orgId: string; + domain: string; + site: string; + siteId: string; + authState: string; + http: boolean; + protocol: string; + proxyPort: number | null; + enabled: boolean; +}; + +type ResourcesTableProps = { + resources: ResourceRow[]; + orgId: string; +}; + +export default function SitesTable({ resources, orgId }: ResourcesTableProps) { + const router = useRouter(); + + const api = createApiClient(useEnvContext()); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedResource, setSelectedResource] = + useState(); + + const deleteResource = (resourceId: number) => { + api.delete(`/resource/${resourceId}`) + .catch((e) => { + console.error("Error deleting resource", e); + toast({ + variant: "destructive", + title: "Error deleting resource", + description: formatAxiosError(e, "Error deleting resource") + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + }); + }; + + async function toggleResourceEnabled(val: boolean, resourceId: number) { + const res = await api + .post>( + `resource/${resourceId}`, + { + enabled: val + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to toggle resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + } + + const columns: ColumnDef[] = [ + { + accessorKey: "dots", + header: "", + cell: ({ row }) => { + const resourceRow = row.original; + const router = useRouter(); + + return ( + + + + + + + + View settings + + + { + setSelectedResource(resourceRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "site", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + + + ); + } + }, + { + accessorKey: "protocol", + header: "Protocol", + cell: ({ row }) => { + const resourceRow = row.original; + return {resourceRow.protocol.toUpperCase()}; + } + }, + { + accessorKey: "domain", + header: "Access", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {!resourceRow.http ? ( + + ) : ( + + )} +
+ ); + } + }, + { + accessorKey: "authState", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {resourceRow.authState === "protected" ? ( + + + Protected + + ) : resourceRow.authState === "not_protected" ? ( + + + Not Protected + + ) : ( + - + )} +
+ ); + } + }, + { + accessorKey: "enabled", + header: "Enabled", + cell: ({ row }) => ( + + toggleResourceEnabled(val, row.original.id) + } + /> + ) + }, + { + id: "actions", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + + +
+ ); + } + } + ]; + + return ( + <> + {selectedResource && ( + { + setIsDeleteModalOpen(val); + setSelectedResource(null); + }} + dialog={ +
+

+ Are you sure you want to remove the resource{" "} + + {selectedResource?.name || + selectedResource?.id} + {" "} + from the organization? +

+ +

+ Once removed, the resource will no longer be + accessible. All targets attached to the resource + will be removed. +

+ +

+ To confirm, please type the name of the resource + below. +

+
+ } + buttonText="Confirm Delete Resource" + onConfirm={async () => deleteResource(selectedResource!.id)} + string={selectedResource.name} + title="Delete Resource" + /> + )} + + { + router.push(`/${orgId}/settings/resources/create`); + }} + /> + + ); +} diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx deleted file mode 100644 index bd38e541..00000000 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ /dev/null @@ -1,983 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { ListRolesResponse } from "@server/routers/role"; -import { toast } from "@app/hooks/useToast"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { AxiosResponse } from "axios"; -import { formatAxiosError } from "@app/lib/api"; -import { - GetResourceWhitelistResponse, - ListResourceRolesResponse, - ListResourceUsersResponse -} from "@server/routers/resource"; -import { Button } from "@app/components/ui/button"; -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 "@app/components/ui/form"; -import { ListUsersResponse } from "@server/routers/user"; -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 { - SettingsContainer, - SettingsSection, - SettingsSectionTitle, - SettingsSectionHeader, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { useRouter } from "next/navigation"; -import { UserType } from "@server/types/UserTypes"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { CheckboxWithLabel } from "@app/components/ui/checkbox"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; - -const UsersRolesFormSchema = z.object({ - roles: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), - users: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - -const whitelistSchema = z.object({ - emails: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - -export default function ResourceAuthenticationPage() { - const { org } = useOrgContext(); - const { resource, updateResource, authInfo, updateAuthInfo } = - useResourceContext(); - - const { env } = useEnvContext(); - - const api = createApiClient({ env }); - const router = useRouter(); - const t = useTranslations(); - - const [pageLoading, setPageLoading] = useState(true); - - const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( - [] - ); - const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>( - [] - ); - const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< - number | null - >(null); - const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< - number | null - >(null); - - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - - const [ssoEnabled, setSsoEnabled] = useState(resource.sso); - // const [blockAccess, setBlockAccess] = useState(resource.blockAccess); - const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled - ); - - const [autoLoginEnabled, setAutoLoginEnabled] = useState( - resource.skipToIdpId !== null && resource.skipToIdpId !== undefined - ); - const [selectedIdpId, setSelectedIdpId] = useState( - resource.skipToIdpId || null - ); - const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]); - - const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); - const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); - - const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = - 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), - defaultValues: { roles: [], users: [] } - }); - - const whitelistForm = useForm({ - resolver: zodResolver(whitelistSchema), - defaultValues: { emails: [] } - }); - - useEffect(() => { - const fetchData = async () => { - try { - const [ - rolesResponse, - resourceRolesResponse, - usersResponse, - resourceUsersResponse, - whitelist, - idpsResponse - ] = await Promise.all([ - api.get>( - `/org/${org?.org.orgId}/roles` - ), - api.get>( - `/resource/${resource.resourceId}/roles` - ), - api.get>( - `/org/${org?.org.orgId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/whitelist` - ), - api.get< - AxiosResponse<{ - idps: { idpId: number; name: string }[]; - }> - >("/idp") - ]); - - setAllRoles( - rolesResponse.data.data.roles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin") - ); - - usersRolesForm.setValue( - "roles", - resourceRolesResponse.data.data.roles - .map((i) => ({ - id: i.roleId.toString(), - text: i.name - })) - .filter((role) => role.text !== "Admin") - ); - - setAllUsers( - usersResponse.data.data.users.map((user) => ({ - id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })) - ); - - usersRolesForm.setValue( - "users", - resourceUsersResponse.data.data.users.map((i) => ({ - id: i.userId.toString(), - text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` - })) - ); - - whitelistForm.setValue( - "emails", - whitelist.data.data.whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - - setAllIdps( - idpsResponse.data.data.idps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })) - ); - - if ( - autoLoginEnabled && - !selectedIdpId && - idpsResponse.data.data.idps.length > 0 - ) { - setSelectedIdpId(idpsResponse.data.data.idps[0].idpId); - } - - setPageLoading(false); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorAuthFetch"), - description: formatAxiosError( - e, - t("resourceErrorAuthFetchDescription") - ) - }); - } - }; - - fetchData(); - }, []); - - async function saveWhitelist() { - setLoadingSaveWhitelist(true); - try { - await api.post(`/resource/${resource.resourceId}`, { - emailWhitelistEnabled: whitelistEnabled - }); - - if (whitelistEnabled) { - await api.post(`/resource/${resource.resourceId}/whitelist`, { - emails: whitelistForm.getValues().emails.map((i) => i.text) - }); - } - - updateResource({ - emailWhitelistEnabled: whitelistEnabled - }); - - toast({ - title: t("resourceWhitelistSave"), - description: t("resourceWhitelistSaveDescription") - }); - router.refresh(); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorWhitelistSave"), - description: formatAxiosError( - e, - t("resourceErrorWhitelistSaveDescription") - ) - }); - } finally { - setLoadingSaveWhitelist(false); - } - } - - async function onSubmitUsersRoles( - data: z.infer - ) { - try { - setLoadingSaveUsersRoles(true); - - // Validate that an IDP is selected if auto login is enabled - if (autoLoginEnabled && !selectedIdpId) { - toast({ - variant: "destructive", - title: t("error"), - description: t("selectIdpRequired") - }); - return; - } - - const jobs = [ - api.post(`/resource/${resource.resourceId}/roles`, { - roleIds: data.roles.map((i) => parseInt(i.id)) - }), - api.post(`/resource/${resource.resourceId}/users`, { - userIds: data.users.map((i) => i.id) - }), - api.post(`/resource/${resource.resourceId}`, { - sso: ssoEnabled, - skipToIdpId: autoLoginEnabled ? selectedIdpId : null - }) - ]; - - await Promise.all(jobs); - - updateResource({ - sso: ssoEnabled, - skipToIdpId: autoLoginEnabled ? selectedIdpId : null - }); - - updateAuthInfo({ - sso: ssoEnabled - }); - - toast({ - title: t("resourceAuthSettingsSave"), - description: t("resourceAuthSettingsSaveDescription") - }); - router.refresh(); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorUsersRolesSave"), - description: formatAxiosError( - e, - t("resourceErrorUsersRolesSaveDescription") - ) - }); - } finally { - setLoadingSaveUsersRoles(false); - } - } - - function removeResourcePassword() { - setLoadingRemoveResourcePassword(true); - - api.post(`/resource/${resource.resourceId}/password`, { - password: null - }) - .then(() => { - toast({ - title: t("resourcePasswordRemove"), - description: t("resourcePasswordRemoveDescription") - }); - - updateAuthInfo({ - password: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPasswordRemove"), - description: formatAxiosError( - e, - t("resourceErrorPasswordRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourcePassword(false)); - } - - function removeResourcePincode() { - setLoadingRemoveResourcePincode(true); - - api.post(`/resource/${resource.resourceId}/pincode`, { - pincode: null - }) - .then(() => { - toast({ - title: t("resourcePincodeRemove"), - description: t("resourcePincodeRemoveDescription") - }); - - updateAuthInfo({ - pincode: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPincodeRemove"), - description: formatAxiosError( - e, - t("resourceErrorPincodeRemoveDescription") - ) - }); - }) - .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 <>; - } - - return ( - <> - {isSetPasswordOpen && ( - { - setIsSetPasswordOpen(false); - updateAuthInfo({ - password: true - }); - }} - /> - )} - - {isSetPincodeOpen && ( - { - setIsSetPincodeOpen(false); - updateAuthInfo({ - pincode: true - }); - }} - /> - )} - - {isSetHeaderAuthOpen && ( - { - setIsSetHeaderAuthOpen(false); - updateAuthInfo({ - headerAuth: true - }); - }} - /> - )} - - - - - - {t("resourceUsersRoles")} - - - {t("resourceUsersRolesDescription")} - - - - - setSsoEnabled(val)} - /> - -
- - {ssoEnabled && ( - <> - ( - - - {t("roles")} - - - { - usersRolesForm.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - - )} - /> - ( - - - {t("users")} - - - { - usersRolesForm.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
-
- { - setAutoLoginEnabled( - checked as boolean - ); - if ( - checked && - allIdps.length > 0 - ) { - setSelectedIdpId( - allIdps[0].id - ); - } else { - setSelectedIdpId( - null - ); - } - }} - /> -

- {t( - "autoLoginExternalIdpDescription" - )} -

-
- - {autoLoginEnabled && ( -
- - -
- )} -
- )} - - -
-
- - - -
- - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - - {/* Password Protection */} -
-
- - - {t("resourcePasswordProtection", { - status: authInfo.password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* PIN Code Protection */} -
-
- - - {t("resourcePincodeProtection", { - status: authInfo.pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header Authentication Protection */} -
-
- - - {authInfo.headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!env.email.emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - - - {whitelistEnabled && env.email.emailEnabled && ( -
- - ( - - - - - - {/* @ts-ignore */} - { - return z - .string() - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: - t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse( - tag - ).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t( - "otpEmailEnter" - )} - tags={ - whitelistForm.getValues() - .emails - } - setTags={( - newRoles - ) => { - whitelistForm.setValue( - "emails", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - allowDuplicates={ - false - } - sortTags={true} - /> - - - {t( - "otpEmailEnterDescription" - )} - - - )} - /> - - - )} -
-
- - - -
-
- - ); -} diff --git a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx deleted file mode 100644 index 66ec23cf..00000000 --- a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx +++ /dev/null @@ -1,489 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { formatAxiosError } from "@app/lib/api"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ListSitesResponse } from "@server/routers/site"; -import { useEffect, useState } from "react"; -import { AxiosResponse } from "axios"; -import { useParams, useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { toast } from "@app/hooks/useToast"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter -} from "@app/components/Settings"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Label } from "@app/components/ui/label"; -import { ListDomainsResponse } from "@server/routers/domain"; -import { UpdateResourceResponse } from "@server/routers/resource"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { useTranslations } from "next-intl"; -import { Checkbox } from "@app/components/ui/checkbox"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import DomainPicker from "@app/components/DomainPicker"; -import { Globe } from "lucide-react"; -import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; -import { DomainRow } from "../../../../../../components/DomainsTable"; -import { toASCII, toUnicode } from "punycode"; - -export default function GeneralForm() { - const [formKey, setFormKey] = useState(0); - const params = useParams(); - const { resource, updateResource } = useResourceContext(); - const { org } = useOrgContext(); - const router = useRouter(); - const t = useTranslations(); - const [editDomainOpen, setEditDomainOpen] = useState(false); - - const { env } = useEnvContext(); - - const orgId = params.orgId; - - const api = createApiClient({ env }); - - const [sites, setSites] = useState([]); - const [saveLoading, setSaveLoading] = useState(false); - const [transferLoading, setTransferLoading] = useState(false); - const [open, setOpen] = useState(false); - const [baseDomains, setBaseDomains] = useState< - ListDomainsResponse["domains"] - >([]); - - const [loadingPage, setLoadingPage] = useState(true); - const [resourceFullDomain, setResourceFullDomain] = useState( - `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` - ); - const [selectedDomain, setSelectedDomain] = useState<{ - domainId: string; - subdomain?: string; - fullDomain: string; - baseDomain: string; - } | null>(null); - - const GeneralFormSchema = z - .object({ - enabled: z.boolean(), - subdomain: z.string().optional(), - name: z.string().min(1).max(255), - domainId: z.string().optional(), - proxyPort: z.number().int().min(1).max(65535).optional(), - // enableProxy: z.boolean().optional() - }) - .refine( - (data) => { - // For non-HTTP resources, proxyPort should be defined - if (!resource.http) { - return data.proxyPort !== undefined; - } - // For HTTP resources, proxyPort should be undefined - return data.proxyPort === undefined; - }, - { - message: !resource.http - ? "Port number is required for non-HTTP resources" - : "Port number should not be set for HTTP resources", - path: ["proxyPort"] - } - ); - - type GeneralFormValues = z.infer; - - const form = useForm({ - resolver: zodResolver(GeneralFormSchema), - defaultValues: { - enabled: resource.enabled, - name: resource.name, - subdomain: resource.subdomain ? resource.subdomain : undefined, - domainId: resource.domainId || undefined, - proxyPort: resource.proxyPort || undefined, - // enableProxy: resource.enableProxy || false - }, - mode: "onChange" - }); - - useEffect(() => { - const fetchSites = async () => { - const res = await api.get>( - `/org/${orgId}/sites/` - ); - setSites(res.data.data.sites); - }; - - const fetchDomains = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${orgId}/domains/`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("domainErrorFetch"), - description: formatAxiosError( - e, - t("domainErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - })); - setBaseDomains(domains); - setFormKey((key) => key + 1); - } - }; - - const load = async () => { - await fetchDomains(); - await fetchSites(); - - setLoadingPage(false); - }; - - load(); - }, []); - - async function onSubmit(data: GeneralFormValues) { - setSaveLoading(true); - - const res = await api - .post>( - `resource/${resource?.resourceId}`, - { - enabled: data.enabled, - name: data.name, - subdomain: data.subdomain ? toASCII(data.subdomain) : undefined, - domainId: data.domainId, - proxyPort: data.proxyPort, - // ...(!resource.http && { - // enableProxy: data.enableProxy - // }) - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorUpdate"), - description: formatAxiosError( - e, - t("resourceErrorUpdateDescription") - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: t("resourceUpdated"), - description: t("resourceUpdatedDescription") - }); - - const resource = res.data.data; - - updateResource({ - enabled: data.enabled, - name: data.name, - subdomain: data.subdomain, - fullDomain: resource.fullDomain, - proxyPort: data.proxyPort, - // ...(!resource.http && { - // enableProxy: data.enableProxy - // }) - }); - - router.refresh(); - } - setSaveLoading(false); - } - - return ( - !loadingPage && ( - <> - - - - - {t("resourceGeneral")} - - - {t("resourceGeneralDescription")} - - - - - -
- - ( - -
- - - form.setValue( - "enabled", - val - ) - } - /> - -
- -
- )} - /> - - ( - - - {t("name")} - - - - - - - )} - /> - - {!resource.http && ( - <> - ( - - - {t( - "resourcePortNumber" - )} - - - - field.onChange( - e - .target - .value - ? parseInt( - e - .target - .value - ) - : undefined - ) - } - /> - - - - {t( - "resourcePortNumberDescription" - )} - - - )} - /> - - {/* {build == "oss" && ( - ( - - - - -
- - {t( - "resourceEnableProxy" - )} - - - {t( - "resourceEnableProxyDescription" - )} - -
-
- )} - /> - )} */} - - )} - - {resource.http && ( -
- -
- - - {resourceFullDomain} - - -
-
- )} - - -
-
- - - - -
-
- - setEditDomainOpen(setOpen)} - > - - - Edit Domain - - Select a domain for your resource - - - - { - const selected = { - domainId: res.domainId, - subdomain: res.subdomain, - fullDomain: res.fullDomain, - baseDomain: res.baseDomain - }; - setSelectedDomain(selected); - }} - /> - - - - - - - - - - - ) - ); -} diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx deleted file mode 100644 index 04db3de5..00000000 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ /dev/null @@ -1,1778 +0,0 @@ -"use client"; - -import { useEffect, useState, use } 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 { AxiosResponse } from "axios"; -import { ListTargetsResponse } from "@server/routers/target/listTargets"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { CreateTargetResponse } from "@server/routers/target"; -import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender, - Row -} from "@tanstack/react-table"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; -import { toast } from "@app/hooks/useToast"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { formatAxiosError } from "@app/lib/api/formatAxiosError"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; -import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { useRouter } from "next/navigation"; -import { isTargetValid } from "@server/lib/validators"; -import { tlsNameSchema } from "@server/lib/schemas"; -import { - CheckIcon, - ChevronsUpDown, - Settings, - Heart, - Check, - CircleCheck, - CircleX, - ArrowRight, - Plus, - MoveRight, - ArrowUp, - Info, - ArrowDown -} from "lucide-react"; -import { ContainersSelector } from "@app/components/ContainersSelector"; -import { useTranslations } from "next-intl"; -import HealthCheckDialog from "@/components/HealthCheckDialog"; -import { DockerManager, DockerState } from "@app/lib/docker"; -import { Container } from "@server/routers/site"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { 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({ 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" - } - ) - .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() -}); - -type LocalTarget = Omit< - ArrayElement & { - new?: boolean; - updated?: boolean; - siteType: string | null; - }, - "protocol" ->; - -export default function ReverseProxyTargets(props: { - params: Promise<{ resourceId: number; orgId: string }>; -}) { - const params = use(props.params); - const t = useTranslations(); - const { env } = useEnvContext(); - - const { resource, updateResource } = useResourceContext(); - - const api = createApiClient(useEnvContext()); - - const [targets, setTargets] = useState([]); - const [targetsToRemove, setTargetsToRemove] = useState([]); - const [sites, setSites] = useState([]); - const [dockerStates, setDockerStates] = useState>( - new Map() - ); - - const initializeDockerForSite = async (siteId: number) => { - if (dockerStates.has(siteId)) { - return; // Already initialized - } - - const dockerManager = new DockerManager(api, siteId); - const dockerState = await dockerManager.initializeDocker(); - - setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); - }; - - const refreshContainersForSite = async (siteId: number) => { - const dockerManager = new DockerManager(api, siteId); - const containers = await dockerManager.fetchContainers(); - - setDockerStates((prev) => { - const newMap = new Map(prev); - const existingState = newMap.get(siteId); - if (existingState) { - newMap.set(siteId, { ...existingState, containers }); - } - return newMap; - }); - }; - - const getDockerStateForSite = (siteId: number): DockerState => { - return ( - dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - } - ); - }; - - const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); - const [targetsLoading, setTargetsLoading] = useState(false); - const [proxySettingsLoading, setProxySettingsLoading] = useState(false); - - const [pageLoading, setPageLoading] = useState(true); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); - const [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({ - setHostHeader: z - .string() - .optional() - .refine( - (data) => { - if (data) { - return tlsNameSchema.safeParse(data).success; - } - return true; - }, - { - message: t("proxyErrorInvalidHeader") - } - ), - headers: z - .array(z.object({ name: z.string(), value: z.string() })) - .nullable() - }); - - const tlsSettingsSchema = z.object({ - ssl: z.boolean(), - tlsServerName: z - .string() - .optional() - .refine( - (data) => { - if (data) { - return tlsNameSchema.safeParse(data).success; - } - return true; - }, - { - message: t("proxyErrorTls") - } - ) - }); - - type ProxySettingsValues = z.infer; - type TlsSettingsValues = z.infer; - type TargetsSettingsValues = z.infer; - - const tlsSettingsForm = useForm({ - resolver: zodResolver(tlsSettingsSchema), - defaultValues: { - ssl: resource.ssl, - tlsServerName: resource.tlsServerName || "" - } - }); - - const proxySettingsForm = useForm({ - resolver: zodResolver(proxySettingsSchema), - defaultValues: { - setHostHeader: resource.setHostHeader || "", - headers: resource.headers - } - }); - - const targetsSettingsForm = useForm({ - resolver: zodResolver(targetsSettingsSchema), - defaultValues: { - stickySession: resource.stickySession - } - }); - - useEffect(() => { - const fetchTargets = async () => { - try { - const res = await api.get>( - `/resource/${resource.resourceId}/targets` - ); - - if (res.status === 200) { - setTargets(res.data.data.targets); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("targetErrorFetch"), - description: formatAxiosError( - err, - t("targetErrorFetchDescription") - ) - }); - } finally { - setPageLoading(false); - } - }; - fetchTargets(); - - const fetchSites = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${params.orgId}/sites`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("sitesErrorFetch"), - description: formatAxiosError( - e, - t("sitesErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - setSites(res.data.data.sites); - - // Initialize Docker for newt sites - const newtSites = res.data.data.sites.filter( - (site) => site.type === "newt" - ); - for (const site of newtSites) { - initializeDockerForSite(site.siteId); - } - - // Sites loaded successfully - } - }; - fetchSites(); - - // const fetchSite = async () => { - // try { - // const res = await api.get>( - // `/site/${resource.siteId}` - // ); - // - // if (res.status === 200) { - // setSite(res.data.data); - // } - // } catch (err) { - // console.error(err); - // toast({ - // variant: "destructive", - // title: t("siteErrorFetch"), - // description: formatAxiosError( - // err, - // t("siteErrorFetchDescription") - // ) - // }); - // } - // }; - // fetchSite(); - }, []); - - // 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( - (target) => - target.ip === data.ip && - target.port === data.port && - target.method === data.method && - target.siteId === data.siteId - ); - - if (isDuplicate) { - toast({ - variant: "destructive", - title: t("targetErrorDuplicate"), - description: t("targetErrorDuplicateDescription") - }); - return; - } - - // if (site && site.type == "wireguard" && site.subnet) { - // // make sure that the target IP is within the site subnet - // const targetIp = data.ip; - // const subnet = site.subnet; - // try { - // if (!isIPInSubnet(targetIp, subnet)) { - // toast({ - // variant: "destructive", - // title: t("targetWireGuardErrorInvalidIp"), - // description: t( - // "targetWireGuardErrorInvalidIpDescription" - // ) - // }); - // return; - // } - // } catch (error) { - // console.error(error); - // toast({ - // variant: "destructive", - // title: t("targetWireGuardErrorInvalidIp"), - // description: t("targetWireGuardErrorInvalidIpDescription") - // }); - // return; - // } - // } - - const site = sites.find((site) => site.siteId === data.siteId); - const isHttp = resource.http; - - const newTarget: LocalTarget = { - ...data, - 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, - 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]); - } - - const removeTarget = (targetId: number) => { - setTargets([ - ...targets.filter((target) => target.targetId !== targetId) - ]); - - if (!targets.find((target) => target.targetId === targetId)?.new) { - setTargetsToRemove([...targetsToRemove, targetId]); - } - }; - - async function updateTarget(targetId: number, data: Partial) { - const site = sites.find((site) => site.siteId === data.siteId); - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? 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); - setProxySettingsLoading(true); - - // Save targets - for (const target of targets) { - const data: any = { - ip: target.ip, - port: target.port, - method: target.method, - enabled: target.enabled, - siteId: target.siteId, - 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 - >(`/resource/${resource.resourceId}/target`, data); - target.targetId = res.data.data.targetId; - target.new = false; - } else if (target.updated) { - await api.post(`/target/${target.targetId}`, data); - target.updated = false; - } - } - - for (const targetId of targetsToRemove) { - await api.delete(`/target/${targetId}`); - } - - if (resource.http) { - // Gather all settings - const stickySessionData = targetsSettingsForm.getValues(); - const tlsData = tlsSettingsForm.getValues(); - const proxyData = proxySettingsForm.getValues(); - - // Combine into one payload - const payload = { - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null - }; - - // Single API call to update all settings - await api.post(`/resource/${resource.resourceId}`, payload); - - // Update local resource context - updateResource({ - ...resource, - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null - }); - } - - toast({ - title: t("settingsUpdated"), - description: t("settingsUpdatedDescription") - }); - - setTargetsToRemove([]); - router.refresh(); - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("settingsErrorUpdate"), - description: formatAxiosError( - err, - t("settingsErrorUpdateDescription") - ) - }); - } finally { - setTargetsLoading(false); - setHttpsTlsLoading(false); - setProxySettingsLoading(false); - } - } - - const getColumns = (): ColumnDef[] => { - const baseColumns: ColumnDef[] = []; - const isHttp = resource.http; - - const priorityColumn: ColumnDef = { - id: "priority", - header: () => ( -
- {t("priority")} - - - - - - -

{t("priorityDescription")}

-
-
-
-
- ), - cell: ({ row }) => { - return ( -
- { - const value = parseInt(e.target.value, 10); - if (value >= 1 && value <= 1000) { - updateTarget(row.original.targetId, { - ...row.original, - priority: value - }); - } - }} - /> -
- ); - }, - 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 - ); - - const handleContainerSelectForTarget = ( - hostname: string, - port?: number - ) => { - updateTarget(row.original.targetId, { - ...row.original, - ip: hostname, - ...(port && { port: port }) - }); - }; - - return ( -
-
- {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} - - - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - - updateTarget( - row.original - .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 - }); - } - }} - /> -
-
- ); - }, - size: 400, - minSize: 350, - maxSize: 500 - }; - - 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 - }) - } - /> -
- ), - 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, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { - pagination: { - pageIndex: 0, - pageSize: 1000 - } - } - }); - - if (pageLoading) { - return <>; - } - - return ( - - - - {t("targets")} - - {t("targetsDescription")} - - - - {targets.length > 0 ? ( - <> -
- - - {table - .getHeaderGroups() - .map((headerGroup) => ( - - {headerGroup.headers.map( - (header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ) - )} - - ))} - - - {table.getRowModel().rows?.length ? ( - table - .getRowModel() - .rows.map((row) => ( - - {row - .getVisibleCells() - .map((cell) => ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {t("targetNoOne")} - - - )} - - {/* */} - {/* {t('targetNoOneDescription')} */} - {/* */} -
-
-
-
- -
- - -
-
-
- - ) : ( -
-

- {t("targetNoOne")} -

- -
- )} -
-
- - {resource.http && ( - - - - {t("proxyAdditional")} - - - {t("proxyAdditionalDescription")} - - - - -
- - {!env.flags.usePangolinDns && ( - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - )} - ( - - - {t("targetTlsSni")} - - - - - - {t( - "targetTlsSniDescription" - )} - - - - )} - /> - - -
- - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - -
- - -
- - ( - - - {t("proxyCustomHeader")} - - - - - - {t( - "proxyCustomHeaderDescription" - )} - - - - )} - /> - ( - - - {t("customHeaders")} - - - { - field.onChange( - value - ); - }} - rows={4} - /> - - - {t( - "customHeadersDescription" - )} - - - - )} - /> - - -
-
-
- )} - -
- -
- - {selectedTargetForHealthCheck && ( - { - if (selectedTargetForHealthCheck) { - console.log(config); - updateTargetHealthCheck( - selectedTargetForHealthCheck.targetId, - config - ); - } - }} - /> - )} -
- ); -} - -function isIPInSubnet(subnet: string, ip: string): boolean { - const [subnetIP, maskBits] = subnet.split("/"); - const mask = parseInt(maskBits); - - if (mask < 0 || mask > 32) { - throw new Error("subnetMaskErrorInvalid"); - } - - // Convert IP addresses to binary numbers - const subnetNum = ipToNumber(subnetIP); - const ipNum = ipToNumber(ip); - - // Calculate subnet mask - const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1); - - // Check if the IP is in the subnet - return (subnetNum & maskNum) === (ipNum & maskNum); -} - -function ipToNumber(ip: string): number { - // Validate IP address format - const parts = ip.split("."); - - if (parts.length !== 4) { - throw new Error("ipAddressErrorInvalidFormat"); - } - - // Convert IP octets to 32-bit number - return parts.reduce((num, octet) => { - const oct = parseInt(octet); - if (isNaN(oct) || oct < 0 || oct > 255) { - throw new Error("ipAddressErrorInvalidOctet"); - } - return (num << 8) + oct; - }, 0); -} diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx deleted file mode 100644 index b8459293..00000000 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ /dev/null @@ -1,903 +0,0 @@ -"use client"; - -import { useEffect, useState, use } 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 { AxiosResponse } from "axios"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender -} from "@tanstack/react-table"; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; -import { toast } from "@app/hooks/useToast"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { formatAxiosError } from "@app/lib/api/formatAxiosError"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionFooter -} from "@app/components/Settings"; -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, ChevronsUpDown } from "lucide-react"; -import { - InfoSection, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { - isValidCIDR, - isValidIP, - isValidUrlGlobPattern -} from "@server/lib/validators"; -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({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.string(), - value: z.string(), - priority: z.coerce.number().int().optional() -}); - -type LocalRule = ArrayElement & { - new?: boolean; - updated?: boolean; -}; - -export default function ResourceRules(props: { - params: Promise<{ resourceId: number }>; -}) { - const params = use(props.params); - const { resource, updateResource } = useResourceContext(); - const api = createApiClient(useEnvContext()); - const [rules, setRules] = useState([]); - const [rulesToRemove, setRulesToRemove] = useState([]); - 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'), - DROP: t('alwaysDeny'), - PASS: t('passToAuth') - } as const; - - const RuleMatch = { - PATH: t('path'), - IP: "IP", - CIDR: t('ipAddressRange'), - GEOIP: t('country') - } as const; - - const addRuleForm = useForm({ - resolver: zodResolver(addRuleSchema), - defaultValues: { - action: "ACCEPT", - match: "IP", - value: "" - } - }); - - useEffect(() => { - const fetchRules = async () => { - try { - const res = await api.get< - AxiosResponse - >(`/resource/${resource.resourceId}/rules`); - if (res.status === 200) { - setRules(res.data.data.rules); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t('rulesErrorFetch'), - description: formatAxiosError( - err, - t('rulesErrorFetchDescription') - ) - }); - } finally { - setPageLoading(false); - } - }; - fetchRules(); - }, []); - - async function addRule(data: z.infer) { - const isDuplicate = rules.some( - (rule) => - rule.action === data.action && - rule.match === data.match && - rule.value === data.value - ); - - if (isDuplicate) { - toast({ - variant: "destructive", - title: t('rulesErrorDuplicate'), - description: t('rulesErrorDuplicateDescription') - }); - return; - } - - if (data.match === "CIDR" && !isValidCIDR(data.value)) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidIpAddressRange'), - description: t('rulesErrorInvalidIpAddressRangeDescription') - }); - setLoading(false); - return; - } - if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidUrl'), - description: t('rulesErrorInvalidUrlDescription') - }); - setLoading(false); - return; - } - if (data.match === "IP" && !isValidIP(data.value)) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), - description: t('rulesErrorInvalidIpAddressDescription') - }); - 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; - if (priority === undefined) { - priority = rules.reduce( - (acc, rule) => (rule.priority > acc ? rule.priority : acc), - 0 - ); - priority++; - } - - const newRule: LocalRule = { - ...data, - ruleId: new Date().getTime(), - new: true, - resourceId: resource.resourceId, - priority, - enabled: true - }; - - setRules([...rules, newRule]); - addRuleForm.reset(); - } - - const removeRule = (ruleId: number) => { - setRules([...rules.filter((rule) => rule.ruleId !== ruleId)]); - if (!rules.find((rule) => rule.ruleId === ruleId)?.new) { - setRulesToRemove([...rulesToRemove, ruleId]); - } - }; - - async function updateRule(ruleId: number, data: Partial) { - setRules( - rules.map((rule) => - rule.ruleId === ruleId - ? { ...rule, ...data, updated: true } - : rule - ) - ); - } - - function getValueHelpText(type: string) { - switch (type) { - case "CIDR": - return t('rulesMatchIpAddressRangeDescription'); - case "IP": - return t('rulesMatchIpAddress'); - case "PATH": - return t('rulesMatchUrl'); - case "GEOIP": - return t('rulesMatchCountry'); - } - } - - async function saveAllSettings() { - try { - setLoading(true); - - // Save rules enabled state - const res = await api - .post(`/resource/${resource.resourceId}`, { - applyRules: rulesEnabled - }) - .catch((err) => { - console.error(err); - toast({ - variant: "destructive", - title: t('rulesErrorUpdate'), - description: formatAxiosError( - err, - t('rulesErrorUpdateDescription') - ) - }); - throw err; - }); - - if (res && res.status === 200) { - updateResource({ applyRules: rulesEnabled }); - } - - // Save rules - for (const rule of rules) { - const data = { - action: rule.action, - match: rule.match, - value: rule.value, - priority: rule.priority, - enabled: rule.enabled - }; - - if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidIpAddressRange'), - description: t('rulesErrorInvalidIpAddressRangeDescription') - }); - setLoading(false); - return; - } - if ( - rule.match === "PATH" && - !isValidUrlGlobPattern(rule.value) - ) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidUrl'), - description: t('rulesErrorInvalidUrlDescription') - }); - setLoading(false); - return; - } - if (rule.match === "IP" && !isValidIP(rule.value)) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), - description: t('rulesErrorInvalidIpAddressDescription') - }); - setLoading(false); - return; - } - - if (rule.priority === undefined) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidPriority'), - description: t('rulesErrorInvalidPriorityDescription') - }); - setLoading(false); - return; - } - - // make sure no duplicate priorities - const priorities = rules.map((r) => r.priority); - if (priorities.length !== new Set(priorities).size) { - toast({ - variant: "destructive", - title: t('rulesErrorDuplicatePriority'), - description: t('rulesErrorDuplicatePriorityDescription') - }); - setLoading(false); - return; - } - - if (rule.new) { - const res = await api.put( - `/resource/${resource.resourceId}/rule`, - data - ); - rule.ruleId = res.data.data.ruleId; - } else if (rule.updated) { - await api.post( - `/resource/${resource.resourceId}/rule/${rule.ruleId}`, - data - ); - } - - setRules([ - ...rules.map((r) => { - const res = { - ...r, - new: false, - updated: false - }; - return res; - }) - ]); - } - - for (const ruleId of rulesToRemove) { - await api.delete( - `/resource/${resource.resourceId}/rule/${ruleId}` - ); - setRules(rules.filter((r) => r.ruleId !== ruleId)); - } - - toast({ - title: t('ruleUpdated'), - description: t('ruleUpdatedDescription') - }); - - setRulesToRemove([]); - router.refresh(); - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t('ruleErrorUpdate'), - description: formatAxiosError( - err, - t('ruleErrorUpdateDescription') - ) - }); - } - setLoading(false); - } - - const columns: ColumnDef[] = [ - { - accessorKey: "priority", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => ( - { - const parsed = z.coerce - .number() - .int() - .optional() - .safeParse(e.target.value); - - if (!parsed.data) { - toast({ - variant: "destructive", - title: t('rulesErrorInvalidIpAddress'), // correct priority or IP? - description: t('rulesErrorInvalidPriorityDescription') - }); - setLoading(false); - return; - } - - updateRule(row.original.ruleId, { - priority: parsed.data - }); - }} - /> - ) - }, - { - accessorKey: "action", - header: t('rulesAction'), - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "match", - header: t('rulesMatchType'), - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "value", - header: t('value'), - cell: ({ row }) => ( - 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 - }) - } - /> - ) - ) - }, - { - accessorKey: "enabled", - header: t('enabled'), - cell: ({ row }) => ( - - updateRule(row.original.ruleId, { enabled: val }) - } - /> - ) - }, - { - id: "actions", - cell: ({ row }) => ( -
- -
- ) - } - ]; - - const table = useReactTable({ - data: rules, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { - pagination: { - pageIndex: 0, - pageSize: 1000 - } - } - }); - - if (pageLoading) { - return <>; - } - - return ( - - {/* */} - {/* */} - {/* {t('rulesAbout')} */} - {/* */} - {/*
*/} - {/*

*/} - {/* {t('rulesAboutDescription')} */} - {/*

*/} - {/*
*/} - {/* */} - {/* */} - {/* {t('rulesActions')} */} - {/*
    */} - {/*
  • */} - {/* */} - {/* {t('rulesActionAlwaysAllow')} */} - {/*
  • */} - {/*
  • */} - {/* */} - {/* {t('rulesActionAlwaysDeny')} */} - {/*
  • */} - {/*
*/} - {/*
*/} - {/* */} - {/* */} - {/* {t('rulesMatchCriteria')} */} - {/* */} - {/*
    */} - {/*
  • */} - {/* {t('rulesMatchCriteriaIpAddress')} */} - {/*
  • */} - {/*
  • */} - {/* {t('rulesMatchCriteriaIpAddressRange')} */} - {/*
  • */} - {/*
  • */} - {/* {t('rulesMatchCriteriaUrl')} */} - {/*
  • */} - {/*
*/} - {/*
*/} - {/*
*/} - {/*
*/} - {/*
*/} - - - - - {t('rulesResource')} - - - {t('rulesResourceDescription')} - - - -
-
- setRulesEnabled(val)} - /> -
- -
- -
- ( - - {t('rulesAction')} - - - - - - )} - /> - ( - - {t('rulesMatchType')} - - - - - - )} - /> - ( - - - - {addRuleForm.watch("match") === "GEOIP" ? ( - - - - - - - - - {t('noCountryFound')} - - {COUNTRIES.map((country) => ( - { - field.onChange(country.code); - setOpenAddRuleCountrySelect(false); - }} - > - - {country.name} ({country.code}) - - ))} - - - - - - ) : ( - - )} - - - - )} - /> - -
-
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {t('rulesNoOne')} - - - )} - - {/* */} - {/* {t('rulesOrder')} */} - {/* */} -
-
-
-
- -
- -
-
- ); -} diff --git a/src/components/CustomDomainInput.tsx b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx similarity index 96% rename from src/components/CustomDomainInput.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx index 171f5683..0764d740 100644 --- a/src/components/CustomDomainInput.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx @@ -9,7 +9,6 @@ import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { toUnicode } from "punycode"; interface DomainOption { baseDomain: string; @@ -92,7 +91,7 @@ export default function CustomDomainInput({ key={option.domainId} value={option.domainId} > - .{toUnicode(option.baseDomain)} + .{option.baseDomain} ))} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx new file mode 100644 index 00000000..86916755 --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ArrowRight, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { Separator } from "@app/components/ui/separator"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import Link from "next/link"; +import { Switch } from "@app/components/ui/switch"; + +type ResourceInfoBoxType = {}; + +export default function ResourceInfoBox({}: ResourceInfoBoxType) { + const { resource, authInfo } = useResourceContext(); + + let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; + + return ( + + + + Resource Information + + + + {resource.http ? ( + <> + + + Authentication + + + {authInfo.password || + authInfo.pincode || + authInfo.sso || + authInfo.whitelist ? ( +
+ + Protected +
+ ) : ( +
+ + Not Protected +
+ )} +
+
+ + URL + + + + + + Site + + {resource.siteName} + + + + ) : ( + <> + + Protocol + + + {resource.protocol.toUpperCase()} + + + + + Port + + + + + + )} + + Visibility + + {resource.enabled ? "Enabled" : "Disabled"} + + +
+
+
+ ); +} diff --git a/src/components/SetResourcePasswordForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx similarity index 84% rename from src/components/SetResourcePasswordForm.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx index 07146865..3bf2966a 100644 --- a/src/components/SetResourcePasswordForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx @@ -28,10 +28,9 @@ import { } from "@app/components/Credenza"; import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; +import { Resource } from "@server/db/schemas"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; const setPasswordFormSchema = z.object({ password: z.string().min(4).max(100) @@ -39,6 +38,10 @@ const setPasswordFormSchema = z.object({ type SetPasswordFormValues = z.infer; +const defaultValues: Partial = { + password: "" +}; + type SetPasswordFormProps = { open: boolean; setOpen: (open: boolean) => void; @@ -53,15 +56,12 @@ export default function SetResourcePasswordForm({ onSetPassword }: SetPasswordFormProps) { const api = createApiClient(useEnvContext()); - const t = useTranslations(); const [loading, setLoading] = useState(false); - const form = useForm({ + const form = useForm({ resolver: zodResolver(setPasswordFormSchema), - defaultValues: { - password: "" - } + defaultValues }); useEffect(() => { @@ -81,17 +81,18 @@ export default function SetResourcePasswordForm({ .catch((e) => { toast({ variant: "destructive", - title: t('resourceErrorPasswordSetup'), + title: "Error setting resource password", description: formatAxiosError( e, - t('resourceErrorPasswordSetupDescription') + "An error occurred while setting the resource password" ) }); }) .then(() => { toast({ - title: t('resourcePasswordSetup'), - description: t('resourcePasswordSetupDescription') + title: "Resource password set", + description: + "The resource password has been set successfully" }); if (onSetPassword) { @@ -113,9 +114,9 @@ export default function SetResourcePasswordForm({ > - {t('resourcePasswordSetupTitle')} + Set Password - {t('resourcePasswordSetupTitleDescription')} + Set a password to protect this resource @@ -130,7 +131,7 @@ export default function SetResourcePasswordForm({ name="password" render={({ field }) => ( - {t('password')} + Password - + diff --git a/src/components/SetResourcePincodeForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx similarity index 88% rename from src/components/SetResourcePincodeForm.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx index d58d0c85..31ccbea6 100644 --- a/src/components/SetResourcePincodeForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx @@ -28,7 +28,7 @@ import { } from "@app/components/Credenza"; import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; +import { Resource } from "@server/db/schemas"; import { InputOTP, InputOTPGroup, @@ -36,7 +36,6 @@ import { } from "@app/components/ui/input-otp"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; const setPincodeFormSchema = z.object({ pincode: z.string().length(6) @@ -44,6 +43,10 @@ const setPincodeFormSchema = z.object({ type SetPincodeFormValues = z.infer; +const defaultValues: Partial = { + pincode: "" +}; + type SetPincodeFormProps = { open: boolean; setOpen: (open: boolean) => void; @@ -61,15 +64,11 @@ export default function SetResourcePincodeForm({ const api = createApiClient(useEnvContext()); - const form = useForm({ + const form = useForm({ resolver: zodResolver(setPincodeFormSchema), - defaultValues: { - pincode: "" - } + defaultValues }); - const t = useTranslations(); - useEffect(() => { if (!open) { return; @@ -87,17 +86,18 @@ export default function SetResourcePincodeForm({ .catch((e) => { toast({ variant: "destructive", - title: t('resourceErrorPincodeSetup'), + title: "Error setting resource PIN code", description: formatAxiosError( e, - t('resourceErrorPincodeSetupDescription') + "An error occurred while setting the resource PIN code" ) }); }) .then(() => { toast({ - title: t('resourcePincodeSetup'), - description: t('resourcePincodeSetupDescription') + title: "Resource PIN code set", + description: + "The resource pincode has been set successfully" }); if (onSetPincode) { @@ -119,9 +119,9 @@ export default function SetResourcePincodeForm({ > - {t('resourcePincodeSetupTitle')} + Set Pincode - {t('resourcePincodeSetupTitleDescription')} + Set a pincode to protect this resource @@ -136,7 +136,7 @@ export default function SetResourcePincodeForm({ name="pincode" render={({ field }) => ( - {t('resourcePincode')} + PIN Code
- + diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx new file mode 100644 index 00000000..0b0535e8 --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -0,0 +1,741 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { ListRolesResponse } from "@server/routers/role"; +import { toast } from "@app/hooks/useToast"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { AxiosResponse } from "axios"; +import { formatAxiosError } from "@app/lib/api"; +import { + GetResourceWhitelistResponse, + ListResourceRolesResponse, + ListResourceUsersResponse +} from "@server/routers/resource"; +import { Button } from "@app/components/ui/button"; +import { set, z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { ListUsersResponse } from "@server/routers/user"; +import { Binary, Key } from "lucide-react"; +import SetResourcePasswordForm from "./SetResourcePasswordForm"; +import SetResourcePincodeForm from "./SetResourcePincodeForm"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionTitle, + SettingsSectionHeader, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionFooter, + SettingsSectionForm +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { useRouter } from "next/navigation"; +import { UserType } from "@server/types/UserTypes"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon } from "lucide-react"; + +const UsersRolesFormSchema = z.object({ + roles: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ), + users: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ) +}); + +const whitelistSchema = z.object({ + emails: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ) +}); + +export default function ResourceAuthenticationPage() { + const { org } = useOrgContext(); + const { resource, updateResource, authInfo, updateAuthInfo } = + useResourceContext(); + + const { env } = useEnvContext(); + + const api = createApiClient({ env }); + const router = useRouter(); + + const [pageLoading, setPageLoading] = useState(true); + + const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( + [] + ); + const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>( + [] + ); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + const [ssoEnabled, setSsoEnabled] = useState(resource.sso); + // const [blockAccess, setBlockAccess] = useState(resource.blockAccess); + const [whitelistEnabled, setWhitelistEnabled] = useState( + resource.emailWhitelistEnabled + ); + + const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); + const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); + + const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = + useState(false); + const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = + useState(false); + + const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); + const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + + const usersRolesForm = useForm>({ + resolver: zodResolver(UsersRolesFormSchema), + defaultValues: { roles: [], users: [] } + }); + + const whitelistForm = useForm>({ + resolver: zodResolver(whitelistSchema), + defaultValues: { emails: [] } + }); + + useEffect(() => { + const fetchData = async () => { + try { + const [ + rolesResponse, + resourceRolesResponse, + usersResponse, + resourceUsersResponse, + whitelist + ] = await Promise.all([ + api.get>( + `/org/${org?.org.orgId}/roles` + ), + api.get>( + `/resource/${resource.resourceId}/roles` + ), + api.get>( + `/org/${org?.org.orgId}/users` + ), + api.get>( + `/resource/${resource.resourceId}/users` + ), + api.get>( + `/resource/${resource.resourceId}/whitelist` + ) + ]); + + setAllRoles( + rolesResponse.data.data.roles + .map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + .filter((role) => role.text !== "Admin") + ); + + usersRolesForm.setValue( + "roles", + resourceRolesResponse.data.data.roles + .map((i) => ({ + id: i.roleId.toString(), + text: i.name + })) + .filter((role) => role.text !== "Admin") + ); + + setAllUsers( + usersResponse.data.data.users.map((user) => ({ + id: user.id.toString(), + text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })) + ); + + usersRolesForm.setValue( + "users", + resourceUsersResponse.data.data.users.map((i) => ({ + id: i.userId.toString(), + text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` + })) + ); + + whitelistForm.setValue( + "emails", + whitelist.data.data.whitelist.map((w) => ({ + id: w.email, + text: w.email + })) + ); + + setPageLoading(false); + } catch (e) { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to fetch data", + description: formatAxiosError( + e, + "An error occurred while fetching the data" + ) + }); + } + }; + + fetchData(); + }, []); + + async function saveWhitelist() { + setLoadingSaveWhitelist(true); + try { + await api.post(`/resource/${resource.resourceId}`, { + emailWhitelistEnabled: whitelistEnabled + }); + + if (whitelistEnabled) { + await api.post(`/resource/${resource.resourceId}/whitelist`, { + emails: whitelistForm.getValues().emails.map((i) => i.text) + }); + } + + updateResource({ + emailWhitelistEnabled: whitelistEnabled + }); + + toast({ + title: "Saved successfully", + description: "Whitelist settings have been saved" + }); + router.refresh(); + } catch (e) { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to save whitelist", + description: formatAxiosError( + e, + "An error occurred while saving the whitelist" + ) + }); + } finally { + setLoadingSaveWhitelist(false); + } + } + + async function onSubmitUsersRoles( + data: z.infer + ) { + try { + setLoadingSaveUsersRoles(true); + + const jobs = [ + api.post(`/resource/${resource.resourceId}/roles`, { + roleIds: data.roles.map((i) => parseInt(i.id)) + }), + api.post(`/resource/${resource.resourceId}/users`, { + userIds: data.users.map((i) => i.id) + }), + api.post(`/resource/${resource.resourceId}`, { + sso: ssoEnabled + }) + ]; + + await Promise.all(jobs); + + updateResource({ + sso: ssoEnabled + }); + + updateAuthInfo({ + sso: ssoEnabled + }); + + toast({ + title: "Saved successfully", + description: "Authentication settings have been saved" + }); + router.refresh(); + } catch (e) { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to set roles", + description: formatAxiosError( + e, + "An error occurred while setting the roles" + ) + }); + } finally { + setLoadingSaveUsersRoles(false); + } + } + + function removeResourcePassword() { + setLoadingRemoveResourcePassword(true); + + api.post(`/resource/${resource.resourceId}/password`, { + password: null + }) + .then(() => { + toast({ + title: "Resource password removed", + description: + "The resource password has been removed successfully" + }); + + updateAuthInfo({ + password: false + }); + router.refresh(); + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error removing resource password", + description: formatAxiosError( + e, + "An error occurred while removing the resource password" + ) + }); + }) + .finally(() => setLoadingRemoveResourcePassword(false)); + } + + function removeResourcePincode() { + setLoadingRemoveResourcePincode(true); + + api.post(`/resource/${resource.resourceId}/pincode`, { + pincode: null + }) + .then(() => { + toast({ + title: "Resource pincode removed", + description: + "The resource password has been removed successfully" + }); + + updateAuthInfo({ + pincode: false + }); + router.refresh(); + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error removing resource pincode", + description: formatAxiosError( + e, + "An error occurred while removing the resource pincode" + ) + }); + }) + .finally(() => setLoadingRemoveResourcePincode(false)); + } + + if (pageLoading) { + return <>; + } + + return ( + <> + {isSetPasswordOpen && ( + { + setIsSetPasswordOpen(false); + updateAuthInfo({ + password: true + }); + }} + /> + )} + + {isSetPincodeOpen && ( + { + setIsSetPincodeOpen(false); + updateAuthInfo({ + pincode: true + }); + }} + /> + )} + + + + + + Users & Roles + + + Configure which users and roles can visit this + resource + + + + setSsoEnabled(val)} + /> + +
+ + {ssoEnabled && ( + <> + ( + + Roles + + { + usersRolesForm.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + Admins can always access + this resource. + + + )} + /> + ( + + Users + + { + usersRolesForm.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + +
+ + + +
+ + + + + Authentication Methods + + + Allow access to the resource via additional auth + methods + + + + {/* Password Protection */} +
+
+ + + Password Protection{" "} + {authInfo.password ? "Enabled" : "Disabled"} + +
+ +
+ + {/* PIN Code Protection */} +
+
+ + + PIN Code Protection{" "} + {authInfo.pincode ? "Enabled" : "Disabled"} + +
+ +
+
+
+ + + + + One-time Passwords + + + Require email-based authentication for resource + access + + + + {!env.email.emailEnabled && ( + + + + SMTP Required + + + SMTP must be enabled on the server to use one-time password authentication. + + + )} + + + {whitelistEnabled && env.email.emailEnabled && ( +
+ + ( + + + + + + {/* @ts-ignore */} + { + return z + .string() + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + "Invalid email address. Wildcard (*) must be the entire local part." + } + ) + ) + .safeParse( + tag + ).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder="Enter an email" + tags={ + whitelistForm.getValues() + .emails + } + setTags={( + newRoles + ) => { + whitelistForm.setValue( + "emails", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={ + false + } + sortTags={true} + /> + + + Press enter to add an + email after typing it in + the input field. + + + )} + /> + + + )} +
+ + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx new file mode 100644 index 00000000..f1e152d5 --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -0,0 +1,726 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { formatAxiosError } from "@app/lib/api"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem +} from "@/components/ui/command"; +import { cn } from "@app/lib/cn"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@/components/ui/popover"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { ListSitesResponse } from "@server/routers/site"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; +import { useParams, useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { toast } from "@app/hooks/useToast"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionForm, + SettingsSectionFooter +} from "@app/components/Settings"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import CustomDomainInput from "../CustomDomainInput"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; +import { Label } from "@app/components/ui/label"; +import { ListDomainsResponse } from "@server/routers/domain"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { + UpdateResourceResponse, + updateResourceRule +} from "@server/routers/resource"; +import { SwitchInput } from "@app/components/SwitchInput"; + +const GeneralFormSchema = z + .object({ + subdomain: z.string().optional(), + name: z.string().min(1).max(255), + proxyPort: z.number().optional(), + http: z.boolean(), + isBaseDomain: z.boolean().optional(), + domainId: z.string().optional() + }) + .refine( + (data) => { + if (!data.http) { + return z + .number() + .int() + .min(1) + .max(65535) + .safeParse(data.proxyPort).success; + } + return true; + }, + { + message: "Invalid port number", + path: ["proxyPort"] + } + ) + .refine( + (data) => { + if (data.http && !data.isBaseDomain) { + return subdomainSchema.safeParse(data.subdomain).success; + } + return true; + }, + { + message: "Invalid subdomain", + path: ["subdomain"] + } + ); + +const TransferFormSchema = z.object({ + siteId: z.number() +}); + +type GeneralFormValues = z.infer; +type TransferFormValues = z.infer; + +export default function GeneralForm() { + const [formKey, setFormKey] = useState(0); + const params = useParams(); + const { resource, updateResource } = useResourceContext(); + const { org } = useOrgContext(); + const router = useRouter(); + + const { env } = useEnvContext(); + + const orgId = params.orgId; + + const api = createApiClient({ env }); + + const [sites, setSites] = useState([]); + const [saveLoading, setSaveLoading] = useState(false); + const [transferLoading, setTransferLoading] = useState(false); + const [open, setOpen] = useState(false); + const [baseDomains, setBaseDomains] = useState< + ListDomainsResponse["domains"] + >([]); + + const [loadingPage, setLoadingPage] = useState(true); + const [domainType, setDomainType] = useState<"subdomain" | "basedomain">( + resource.isBaseDomain ? "basedomain" : "subdomain" + ); + + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + name: resource.name, + subdomain: resource.subdomain ? resource.subdomain : undefined, + proxyPort: resource.proxyPort ? resource.proxyPort : undefined, + http: resource.http, + isBaseDomain: resource.isBaseDomain ? true : false, + domainId: resource.domainId || undefined + }, + mode: "onChange" + }); + + const transferForm = useForm({ + resolver: zodResolver(TransferFormSchema), + defaultValues: { + siteId: resource.siteId ? Number(resource.siteId) : undefined + } + }); + + useEffect(() => { + const fetchSites = async () => { + const res = await api.get>( + `/org/${orgId}/sites/` + ); + setSites(res.data.data.sites); + }; + + const fetchDomains = async () => { + const res = await api + .get< + AxiosResponse + >(`/org/${orgId}/domains/`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error fetching domains", + description: formatAxiosError( + e, + "An error occurred when fetching the domains" + ) + }); + }); + + if (res?.status === 200) { + const domains = res.data.data.domains; + setBaseDomains(domains); + setFormKey((key) => key + 1); + } + }; + + const load = async () => { + await fetchDomains(); + await fetchSites(); + + setLoadingPage(false); + }; + + load(); + }, []); + + async function onSubmit(data: GeneralFormValues) { + setSaveLoading(true); + + const res = await api + .post>( + `resource/${resource?.resourceId}`, + { + name: data.name, + subdomain: data.http ? data.subdomain : undefined, + proxyPort: data.proxyPort, + isBaseDomain: data.http ? data.isBaseDomain : undefined, + domainId: data.http ? data.domainId : undefined + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to update resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: "Resource updated", + description: "The resource has been updated successfully" + }); + + const resource = res.data.data; + + updateResource({ + name: data.name, + subdomain: data.subdomain, + proxyPort: data.proxyPort, + isBaseDomain: data.isBaseDomain, + fullDomain: resource.fullDomain + }); + + router.refresh(); + } + setSaveLoading(false); + } + + async function onTransfer(data: TransferFormValues) { + setTransferLoading(true); + + const res = await api + .post(`resource/${resource?.resourceId}/transfer`, { + siteId: data.siteId + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to transfer resource", + description: formatAxiosError( + e, + "An error occurred while transferring the resource" + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: "Resource transferred", + description: "The resource has been transferred successfully" + }); + router.refresh(); + + updateResource({ + siteName: + sites.find((site) => site.siteId === data.siteId)?.name || + "" + }); + } + setTransferLoading(false); + } + + async function toggleResourceEnabled(val: boolean) { + const res = await api + .post>( + `resource/${resource.resourceId}`, + { + enabled: val + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to toggle resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + + updateResource({ + enabled: val + }); + } + + return ( + !loadingPage && ( + + + + Visibility + + Completely enable or disable resource visibility + + + + { + await toggleResourceEnabled(val); + }} + /> + + + + + + + General Settings + + + Configure the general settings for this resource + + + + + +
+ + ( + + Name + + + + + + )} + /> + + {resource.http && ( + <> + {env.flags + .allowBaseDomainResources && ( + ( + + + Domain Type + + + + + )} + /> + )} + +
+ {domainType === "subdomain" ? ( +
+ + Subdomain + +
+
+ ( + + + + + + + )} + /> +
+
+ ( + + + + + )} + /> +
+
+
+ ) : ( + ( + + + Base Domain + + + + + )} + /> + )} +
+ + )} + + {!resource.http && ( + ( + + + Port Number + + + + field.onChange( + e.target + .value + ? parseInt( + e + .target + .value + ) + : null + ) + } + /> + + + + )} + /> + )} + + +
+
+ + + + +
+ + + + + Transfer Resource + + + Transfer this resource to a different site + + + + + +
+ + ( + + + Destination Site + + + + + + + + + + + + No sites found. + + + {sites.map( + (site) => ( + { + transferForm.setValue( + "siteId", + site.siteId + ); + setOpen( + false + ); + }} + > + { + site.name + } + + + ) + )} + + + + + + + )} + /> + + +
+
+ + + + +
+
+ ) + ); +} diff --git a/src/app/[orgId]/settings/resources/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx similarity index 70% rename from src/app/[orgId]/settings/resources/[niceId]/layout.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index 8c140333..edb21303 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -12,18 +12,24 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { cache } from "react"; -import ResourceInfoBox from "../../../../../components/ResourceInfoBox"; -import { GetSiteResponse } from "@server/routers/site"; -import { getTranslations } from 'next-intl/server'; +import ResourceInfoBox from "./ResourceInfoBox"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import Link from "next/link"; interface ResourceLayoutProps { children: React.ReactNode; - params: Promise<{ niceId: string; orgId: string }>; + params: Promise<{ resourceId: number | string; orgId: string }>; } export default async function ResourceLayout(props: ResourceLayoutProps) { const params = await props.params; - const t = await getTranslations(); const { children } = props; @@ -31,7 +37,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { let resource = null; try { const res = await internal.get>( - `/org/${params.orgId}/resource/${params.niceId}`, + `/resource/${params.resourceId}`, await authCookieHeader() ); resource = res.data.data; @@ -46,7 +52,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { try { const res = await internal.get< AxiosResponse - >(`/resource/${resource.resourceGuid}/auth`, await authCookieHeader()); + >(`/resource/${resource.resourceId}/auth`, await authCookieHeader()); authInfo = res.data.data; } catch { redirect(`/${params.orgId}/settings/resources`); @@ -76,38 +82,35 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { const navItems = [ { - title: t('general'), - href: `/{orgId}/settings/resources/{niceId}/general` + title: "General", + href: `/{orgId}/settings/resources/{resourceId}/general` }, { - title: t('proxy'), - href: `/{orgId}/settings/resources/{niceId}/proxy` + title: "Proxy", + href: `/{orgId}/settings/resources/{resourceId}/proxy` } ]; if (resource.http) { navItems.push({ - title: t('authentication'), - href: `/{orgId}/settings/resources/{niceId}/authentication` + title: "Authentication", + href: `/{orgId}/settings/resources/{resourceId}/authentication` }); navItems.push({ - title: t('rules'), - href: `/{orgId}/settings/resources/{niceId}/rules` + title: "Rules", + href: `/{orgId}/settings/resources/{resourceId}/rules` }); } return ( <> - +
diff --git a/src/app/[orgId]/settings/resources/[niceId]/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx similarity index 53% rename from src/app/[orgId]/settings/resources/[niceId]/page.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/page.tsx index 41995661..a0d45a94 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx @@ -1,10 +1,10 @@ import { redirect } from "next/navigation"; export default async function ResourcePage(props: { - params: Promise<{ niceId: string; orgId: string }>; + params: Promise<{ resourceId: number | string; orgId: string }>; }) { const params = await props.params; redirect( - `/${params.orgId}/settings/resources/${params.niceId}/proxy` + `/${params.orgId}/settings/resources/${params.resourceId}/proxy` ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx new file mode 100644 index 00000000..90e05ff8 --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -0,0 +1,961 @@ +"use client"; + +import { useEffect, useState, use } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { AxiosResponse } from "axios"; +import { ListTargetsResponse } from "@server/routers/target/listTargets"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { CreateTargetResponse } from "@server/routers/target"; +import { + ColumnDef, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + getCoreRowModel, + useReactTable, + flexRender +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { toast } from "@app/hooks/useToast"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/lib/api"; +import { GetSiteResponse } from "@server/routers/site"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionFooter, + SettingsSectionForm +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { useRouter } from "next/navigation"; +import { isTargetValid } from "@server/lib/validators"; +import { tlsNameSchema } from "@server/lib/schemas"; +import { ChevronsUpDown } from "lucide-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; + +const addTargetSchema = z.object({ + ip: z.string().refine(isTargetValid), + method: z.string().nullable(), + port: z.coerce.number().int().positive() +}); + +const targetsSettingsSchema = z.object({ + stickySession: z.boolean() +}); + +type LocalTarget = Omit< + ArrayElement & { + new?: boolean; + updated?: boolean; + }, + "protocol" +>; + +const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." + } + ) +}); + +const tlsSettingsSchema = z.object({ + ssl: z.boolean(), + tlsServerName: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." + } + ) +}); + +type ProxySettingsValues = z.infer; +type TlsSettingsValues = z.infer; +type TargetsSettingsValues = z.infer; + +export default function ReverseProxyTargets(props: { + params: Promise<{ resourceId: number }>; +}) { + const params = use(props.params); + + const { resource, updateResource } = useResourceContext(); + + const api = createApiClient(useEnvContext()); + + const [targets, setTargets] = useState([]); + const [site, setSite] = useState(); + const [targetsToRemove, setTargetsToRemove] = useState([]); + + const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); + const [targetsLoading, setTargetsLoading] = useState(false); + const [proxySettingsLoading, setProxySettingsLoading] = useState(false); + + const [pageLoading, setPageLoading] = useState(true); + const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); + const router = useRouter(); + + const addTargetForm = useForm({ + resolver: zodResolver(addTargetSchema), + defaultValues: { + ip: "", + method: resource.http ? "http" : null, + port: "" as any as number + } as z.infer + }); + + const tlsSettingsForm = useForm({ + resolver: zodResolver(tlsSettingsSchema), + defaultValues: { + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "" + } + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "" + } + }); + + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: resource.stickySession + } + }); + + useEffect(() => { + const fetchTargets = async () => { + try { + const res = await api.get>( + `/resource/${params.resourceId}/targets` + ); + + if (res.status === 200) { + setTargets(res.data.data.targets); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to fetch targets", + description: formatAxiosError( + err, + "An error occurred while fetching targets" + ) + }); + } finally { + setPageLoading(false); + } + }; + fetchTargets(); + + const fetchSite = async () => { + try { + const res = await api.get>( + `/site/${resource.siteId}` + ); + + if (res.status === 200) { + setSite(res.data.data); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to fetch resource", + description: formatAxiosError( + err, + "An error occurred while fetching resource" + ) + }); + } + }; + fetchSite(); + }, []); + + async function addTarget(data: z.infer) { + // Check if target with same IP, port and method already exists + const isDuplicate = targets.some( + (target) => + target.ip === data.ip && + target.port === data.port && + target.method === data.method + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: "Duplicate target", + description: "A target with these settings already exists" + }); + return; + } + + if (site && site.type == "wireguard" && site.subnet) { + // make sure that the target IP is within the site subnet + const targetIp = data.ip; + const subnet = site.subnet; + if (!isIPInSubnet(targetIp, subnet)) { + toast({ + variant: "destructive", + title: "Invalid target IP", + description: "Target IP must be within the site subnet" + }); + return; + } + } + + const newTarget: LocalTarget = { + ...data, + enabled: true, + targetId: new Date().getTime(), + new: true, + resourceId: resource.resourceId + }; + + setTargets([...targets, newTarget]); + addTargetForm.reset({ + ip: "", + method: resource.http ? "http" : null, + port: "" as any as number + }); + } + + const removeTarget = (targetId: number) => { + setTargets([ + ...targets.filter((target) => target.targetId !== targetId) + ]); + + if (!targets.find((target) => target.targetId === targetId)?.new) { + setTargetsToRemove([...targetsToRemove, targetId]); + } + }; + + async function updateTarget(targetId: number, data: Partial) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { ...target, ...data, updated: true } + : target + ) + ); + } + + async function saveTargets() { + try { + setTargetsLoading(true); + + for (let target of targets) { + const data = { + ip: target.ip, + port: target.port, + method: target.method, + enabled: target.enabled + }; + + if (target.new) { + const res = await api.put< + AxiosResponse + >(`/resource/${params.resourceId}/target`, data); + target.targetId = res.data.data.targetId; + } else if (target.updated) { + await api.post(`/target/${target.targetId}`, data); + } + } + + for (const targetId of targetsToRemove) { + await api.delete(`/target/${targetId}`); + } + + // Save sticky session setting + const stickySessionData = targetsSettingsForm.getValues(); + await api.post(`/resource/${params.resourceId}`, { + stickySession: stickySessionData.stickySession + }); + updateResource({ stickySession: stickySessionData.stickySession }); + + toast({ + title: "Targets updated", + description: "Targets and settings updated successfully" + }); + + setTargetsToRemove([]); + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update targets", + description: formatAxiosError( + err, + "An error occurred while updating targets" + ) + }); + } finally { + setTargetsLoading(false); + } + } + + async function saveTlsSettings(data: TlsSettingsValues) { + try { + setHttpsTlsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + ssl: data.ssl, + tlsServerName: data.tlsServerName || null + }); + updateResource({ + ...resource, + ssl: data.ssl, + tlsServerName: data.tlsServerName || null + }); + toast({ + title: "TLS settings updated", + description: "Your TLS settings have been updated successfully" + }); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update TLS settings", + description: formatAxiosError( + err, + "An error occurred while updating TLS settings" + ) + }); + } finally { + setHttpsTlsLoading(false); + } + } + + async function saveProxySettings(data: ProxySettingsValues) { + try { + setProxySettingsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + setHostHeader: data.setHostHeader || null + }); + updateResource({ + ...resource, + setHostHeader: data.setHostHeader || null + }); + toast({ + title: "Proxy settings updated", + description: + "Your proxy settings have been updated successfully" + }); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update proxy settings", + description: formatAxiosError( + err, + "An error occurred while updating proxy settings" + ) + }); + } finally { + setProxySettingsLoading(false); + } + } + + const columns: ColumnDef[] = [ + { + accessorKey: "ip", + header: "IP / Hostname", + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ip: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "port", + header: "Port", + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + port: parseInt(e.target.value, 10) + }) + } + /> + ) + }, + // { + // accessorKey: "protocol", + // header: "Protocol", + // cell: ({ row }) => ( + // + // ), + // }, + { + accessorKey: "enabled", + header: "Enabled", + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + cell: ({ row }) => ( + <> +
+ {/* */} + + +
+ + ) + } + ]; + + if (resource.http) { + const methodCol: ColumnDef = { + accessorKey: "method", + header: "Method", + cell: ({ row }) => ( + + ) + }; + + // add this to the first column + columns.unshift(methodCol); + } + + const table = useReactTable({ + data: targets, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + pagination: { + pageIndex: 0, + pageSize: 1000 + } + } + }); + + if (pageLoading) { + return <>; + } + + return ( + + {resource.http && ( + + + + HTTPS & TLS Settings + + + Configure TLS settings for your resource + + + + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + +
+ + + +
+ + ( + + + TLS Server Name + (SNI) + + + + + + The TLS Server Name + to use for SNI. + Leave empty to use + the default. + + + + )} + /> + +
+ + +
+
+ + + +
+ )} + + + + + Targets Configuration + + + Set up targets to route traffic to your services + + + + +
+ + {targets.length >= 2 && ( + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + )} + + +
+ +
+ +
+ {resource.http && ( + ( + + Method + + + + + + )} + /> + )} + + ( + + IP / Hostname + + + + + + )} + /> + ( + + Port + + + + + + )} + /> + +
+
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No targets. Add a target using the form. + + + )} + + + Adding more than one target above will enable load + balancing. + +
+
+ + + +
+ + {resource.http && ( + + + + Additional Proxy Settings + + + Configure how your resource handles proxy settings + + + + +
+ + ( + + + Custom Host Header + + + + + + The host header to set when + proxying requests. Leave + empty to use the default. + + + + )} + /> + + +
+
+ + + +
+ )} +
+ ); +} + +function isIPInSubnet(subnet: string, ip: string): boolean { + // Split subnet into IP and mask parts + const [subnetIP, maskBits] = subnet.split("/"); + const mask = parseInt(maskBits); + + if (mask < 0 || mask > 32) { + throw new Error("Invalid subnet mask. Must be between 0 and 32."); + } + + // Convert IP addresses to binary numbers + const subnetNum = ipToNumber(subnetIP); + const ipNum = ipToNumber(ip); + + // Calculate subnet mask + const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1); + + // Check if the IP is in the subnet + return (subnetNum & maskNum) === (ipNum & maskNum); +} + +function ipToNumber(ip: string): number { + // Validate IP address format + const parts = ip.split("."); + if (parts.length !== 4) { + throw new Error("Invalid IP address format"); + } + + // Convert IP octets to 32-bit number + return parts.reduce((num, octet) => { + const oct = parseInt(octet); + if (isNaN(oct) || oct < 0 || oct > 255) { + throw new Error("Invalid IP address octet"); + } + return (num << 8) + oct; + }, 0); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx new file mode 100644 index 00000000..2a9fa00f --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -0,0 +1,785 @@ +"use client"; +import { useEffect, useState, use } 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 { AxiosResponse } from "axios"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + ColumnDef, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + getCoreRowModel, + useReactTable, + flexRender +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { toast } from "@app/hooks/useToast"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/lib/api"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionFooter +} from "@app/components/Settings"; +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 { + InfoSection, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { Separator } from "@app/components/ui/separator"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { Switch } from "@app/components/ui/switch"; +import { useRouter } from "next/navigation"; + +// Schema for rule validation +const addRuleSchema = z.object({ + action: z.string(), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = ArrayElement & { + new?: boolean; + updated?: boolean; +}; + +enum RuleAction { + ACCEPT = "Always Allow", + DROP = "Always Deny" +} + +enum RuleMatch { + PATH = "Path", + IP = "IP", + CIDR = "IP Range" +} + +export default function ResourceRules(props: { + params: Promise<{ resourceId: number }>; +}) { + const params = use(props.params); + const { resource, updateResource } = useResourceContext(); + const api = createApiClient(useEnvContext()); + const [rules, setRules] = useState([]); + const [rulesToRemove, setRulesToRemove] = useState([]); + const [loading, setLoading] = useState(false); + const [pageLoading, setPageLoading] = useState(true); + const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules); + const router = useRouter(); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT", + match: "IP", + value: "" + } + }); + + useEffect(() => { + const fetchRules = async () => { + try { + const res = await api.get< + AxiosResponse + >(`/resource/${params.resourceId}/rules`); + if (res.status === 200) { + setRules(res.data.data.rules); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to fetch rules", + description: formatAxiosError( + err, + "An error occurred while fetching rules" + ) + }); + } finally { + setPageLoading(false); + } + }; + fetchRules(); + }, []); + + async function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: "Duplicate rule", + description: "A rule with these settings already exists" + }); + return; + } + + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: "Invalid CIDR", + description: "Please enter a valid CIDR value" + }); + setLoading(false); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: "Invalid URL path", + description: "Please enter a valid URL path value" + }); + setLoading(false); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: "Invalid IP", + description: "Please enter a valid IP address" + }); + setLoading(false); + return; + } + + // find the highest priority and add one + let priority = data.priority; + if (priority === undefined) { + priority = rules.reduce( + (acc, rule) => (rule.priority > acc ? rule.priority : acc), + 0 + ); + priority++; + } + + const newRule: LocalRule = { + ...data, + ruleId: new Date().getTime(), + new: true, + resourceId: resource.resourceId, + priority, + enabled: true + }; + + setRules([...rules, newRule]); + addRuleForm.reset(); + } + + const removeRule = (ruleId: number) => { + setRules([...rules.filter((rule) => rule.ruleId !== ruleId)]); + if (!rules.find((rule) => rule.ruleId === ruleId)?.new) { + setRulesToRemove([...rulesToRemove, ruleId]); + } + }; + + async function updateRule(ruleId: number, data: Partial) { + setRules( + rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ) + ); + } + + async function saveApplyRules(val: boolean) { + const res = await api + .post(`/resource/${params.resourceId}`, { + applyRules: val + }) + .catch((err) => { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update rules", + description: formatAxiosError( + err, + "An error occurred while updating rules" + ) + }); + }); + + if (res && res.status === 200) { + setRulesEnabled(val); + updateResource({ applyRules: val }); + + toast({ + title: "Enable Rules", + description: "Rule evaluation has been updated" + }); + router.refresh(); + } + } + + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return "Enter an address in CIDR format (e.g., 103.21.244.0/22)"; + case "IP": + return "Enter an IP address (e.g., 103.21.244.12)"; + case "PATH": + return "Enter a URL path or pattern (e.g., /api/v1/todos or /api/v1/*)"; + } + } + + async function saveRules() { + try { + setLoading(true); + for (let rule of rules) { + const data = { + action: rule.action, + match: rule.match, + value: rule.value, + priority: rule.priority, + enabled: rule.enabled + }; + + if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { + toast({ + variant: "destructive", + title: "Invalid CIDR", + description: "Please enter a valid CIDR value" + }); + setLoading(false); + return; + } + if ( + rule.match === "PATH" && + !isValidUrlGlobPattern(rule.value) + ) { + toast({ + variant: "destructive", + title: "Invalid URL path", + description: "Please enter a valid URL path value" + }); + setLoading(false); + return; + } + if (rule.match === "IP" && !isValidIP(rule.value)) { + toast({ + variant: "destructive", + title: "Invalid IP", + description: "Please enter a valid IP address" + }); + setLoading(false); + return; + } + + if (rule.priority === undefined) { + toast({ + variant: "destructive", + title: "Invalid Priority", + description: "Please enter a valid priority" + }); + setLoading(false); + return; + } + + // make sure no duplicate priorities + const priorities = rules.map((r) => r.priority); + if (priorities.length !== new Set(priorities).size) { + toast({ + variant: "destructive", + title: "Duplicate Priorities", + description: "Please enter unique priorities" + }); + setLoading(false); + return; + } + + if (rule.new) { + const res = await api.put( + `/resource/${params.resourceId}/rule`, + data + ); + rule.ruleId = res.data.data.ruleId; + } else if (rule.updated) { + await api.post( + `/resource/${params.resourceId}/rule/${rule.ruleId}`, + data + ); + } + + setRules([ + ...rules.map((r) => { + let res = { + ...r, + new: false, + updated: false + }; + return res; + }) + ]); + } + + for (const ruleId of rulesToRemove) { + await api.delete( + `/resource/${params.resourceId}/rule/${ruleId}` + ); + setRules(rules.filter((r) => r.ruleId !== ruleId)); + } + + toast({ + title: "Rules updated", + description: "Rules updated successfully" + }); + + setRulesToRemove([]); + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Operation failed", + description: formatAxiosError( + err, + "An error occurred during the save operation" + ) + }); + } + setLoading(false); + } + + const columns: ColumnDef[] = [ + { + accessorKey: "priority", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( + { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + + if (!parsed.data) { + toast({ + variant: "destructive", + title: "Invalid IP", + description: "Please enter a valid priority" + }); + setLoading(false); + return; + } + + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: "Action", + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: "Match Type", + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: "Value", + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: "Enabled", + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + cell: ({ row }) => ( +
+ +
+ ) + } + ]; + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + pagination: { + pageIndex: 0, + pageSize: 1000 + } + } + }); + + if (pageLoading) { + return <>; + } + + return ( + + + + About Rules + +
+

+ 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. +

+
+ + + Actions +
    +
  • + + Always Allow: Bypass all authentication + methods +
  • +
  • + + Always Deny: Block all requests; no + authentication can be attempted +
  • +
+
+ + + Matching Criteria + +
    +
  • + Match a specific IP address +
  • +
  • + Match a range of IP addresses in CIDR + notation +
  • +
  • + Match a URL path or pattern +
  • +
+
+
+
+
+ + + + Enable Rules + + Enable or disable rule evaluation for this resource + + + + { + await saveApplyRules(val); + }} + /> + + + + + + + Resource Rules Configuration + + + Configure rules to control access to your resource + + + +
+ +
+ ( + + Action + + + + + + )} + /> + ( + + Match Type + + + + + + )} + /> + ( + + + + + + + + )} + /> + +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No rules. Add a rule using the form. + + + )} + + + Rules are evaluated by priority in ascending order. + +
+
+ + + +
+
+ ); +} diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index e4755f3b..c1be6353 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -32,7 +32,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; +import { Resource } from "@server/db/schemas"; import { StrategySelect } from "@app/components/StrategySelect"; import { Select, @@ -41,7 +41,9 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { subdomainSchema } from "@server/lib/schemas"; import { ListDomainsResponse } from "@server/routers/domain"; +import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; import { Command, CommandEmpty, @@ -57,148 +59,33 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { - ArrowRight, - CircleCheck, - CircleX, - Info, - MoveRight, - Plus, - Settings, - SquareArrowOutUpRight -} from "lucide-react"; +import { 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 { ContainersSelector } from "@app/components/ContainersSelector"; -import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender, - Row -} from "@tanstack/react-table"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; -import { Switch } from "@app/components/ui/switch"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { isTargetValid } from "@server/lib/validators"; -import { ListTargetsResponse } from "@server/routers/target"; -import { DockerManager, DockerState } from "@app/lib/docker"; -import { parseHostTarget } from "@app/lib/parseHostTarget"; -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), + siteId: z.number(), http: z.boolean() }); -const httpResourceFormSchema = z.object({ - domainId: z.string().nonempty(), - subdomain: z.string().optional() -}); +const httpResourceFormSchema = z.discriminatedUnion("isBaseDomain", [ + z.object({ + isBaseDomain: z.literal(true), + domainId: z.string().min(1) + }), + z.object({ + isBaseDomain: z.literal(false), + domainId: z.string().min(1), + subdomain: z.string().pipe(subdomainSchema) + }) +]); const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), proxyPort: z.number().int().min(1).max(65535) - // enableProxy: z.boolean().default(false) }); -const addTargetSchema = z - .object({ - ip: z.string().refine(isTargetValid), - method: z.string().nullable(), - port: z.coerce.number().int().positive(), - siteId: z.number().int().positive(), - 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" - } - ) - .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; type TcpUdpResourceFormValues = z.infer; @@ -212,21 +99,11 @@ interface ResourceTypeOption { disabled?: boolean; } -type LocalTarget = Omit< - ArrayElement & { - new?: boolean; - updated?: boolean; - siteType: string | null; - }, - "protocol" ->; - export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); const { orgId } = useParams(); const router = useRouter(); - const t = useTranslations(); const [loadingPage, setLoadingPage] = useState(true); const [sites, setSites] = useState([]); @@ -235,93 +112,25 @@ export default function Page() { >([]); const [createLoading, setCreateLoading] = useState(false); const [showSnippets, setShowSnippets] = useState(false); - const [niceId, setNiceId] = useState(""); - - // Target management state - const [targets, setTargets] = useState([]); - const [targetsToRemove, setTargetsToRemove] = useState([]); - 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 [resourceId, setResourceId] = useState(null); const resourceTypes: ReadonlyArray = [ { id: "http", - title: t("resourceHTTP"), - description: t("resourceHTTPDescription") + title: "HTTPS Resource", + description: + "Proxy requests to your app over HTTPS using a subdomain or base domain." }, - ...(!env.flags.allowRawResources - ? [] - : [ - { - id: "raw" as ResourceType, - title: t("resourceRaw"), - description: t("resourceRawDescription") - } - ]) + { + id: "raw", + title: "Raw TCP/UDP Resource", + description: + "Proxy requests to your app over TCP/UDP using a port number.", + disabled: !env.flags.allowRawResources + } ]; - const baseForm = useForm({ + const baseForm = useForm({ resolver: zodResolver(baseResourceFormSchema), defaultValues: { name: "", @@ -329,190 +138,23 @@ export default function Page() { } }); - const httpForm = useForm({ + const httpForm = useForm({ resolver: zodResolver(httpResourceFormSchema), - defaultValues: {} + defaultValues: { + subdomain: "", + domainId: "", + isBaseDomain: false + } }); - const tcpUdpForm = useForm({ + const tcpUdpForm = useForm({ resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", proxyPort: undefined - // enableProxy: false } }); - const addTargetForm = useForm({ - resolver: zodResolver(addTargetSchema), - defaultValues: { - ip: "", - method: baseForm.watch("http") ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null, - rewritePath: null, - rewritePathType: null, - priority: baseForm.watch("http") ? 100 : undefined - } as z.infer - }); - - // 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 - - 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) => { - if (dockerStates.has(siteId)) { - return; // Already initialized - } - - const dockerManager = new DockerManager(api, siteId); - const dockerState = await dockerManager.initializeDocker(); - - setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); - }; - - const refreshContainersForSite = async (siteId: number) => { - const dockerManager = new DockerManager(api, siteId); - const containers = await dockerManager.fetchContainers(); - - setDockerStates((prev) => { - const newMap = new Map(prev); - const existingState = newMap.get(siteId); - if (existingState) { - newMap.set(siteId, { ...existingState, containers }); - } - return newMap; - }); - }; - - const getDockerStateForSite = (siteId: number): DockerState => { - return ( - dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - } - ); - }; - - async function addTarget(data: z.infer) { - // Check if target with same IP, port and method already exists - const isDuplicate = targets.some( - (target) => - target.ip === data.ip && - target.port === data.port && - target.method === data.method && - target.siteId === data.siteId - ); - - if (isDuplicate) { - toast({ - variant: "destructive", - title: t("targetErrorDuplicate"), - description: t("targetErrorDuplicateDescription") - }); - return; - } - - const site = sites.find((site) => site.siteId === data.siteId); - - const isHttp = baseForm.watch("http"); - - const newTarget: LocalTarget = { - ...data, - 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 - 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]); - addTargetForm.reset({ - ip: "", - method: baseForm.watch("http") ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null, - rewritePath: null, - rewritePathType: null, - priority: isHttp ? 100 : undefined - }); - } - - const removeTarget = (targetId: number) => { - setTargets([ - ...targets.filter((target) => target.targetId !== targetId) - ]); - - if (!targets.find((target) => target.targetId === targetId)?.new) { - setTargetsToRemove([...targetsToRemove, targetId]); - } - }; - - async function updateTarget(targetId: number, data: Partial) { - const site = sites.find((site) => site.siteId === data.siteId); - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ) - ); - } - async function onSubmit() { setCreateLoading(true); @@ -522,122 +164,66 @@ export default function Page() { try { const payload = { name: baseData.name, - http: baseData.http, + siteId: baseData.siteId, + http: baseData.http }; - let sanitizedSubdomain: string | undefined; - if (isHttp) { const httpData = httpForm.getValues(); - - sanitizedSubdomain = httpData.subdomain - ? finalizeSubdomainSanitize(httpData.subdomain) - : undefined; - - Object.assign(payload, { - subdomain: sanitizedSubdomain - ? toASCII(sanitizedSubdomain) - : undefined, - domainId: httpData.domainId, - protocol: "tcp" - }); + if (httpData.isBaseDomain) { + Object.assign(payload, { + domainId: httpData.domainId, + isBaseDomain: true, + protocol: "tcp" + }); + } else { + Object.assign(payload, { + subdomain: httpData.subdomain, + domainId: httpData.domainId, + isBaseDomain: false, + protocol: "tcp" + }); + } } else { const tcpUdpData = tcpUdpForm.getValues(); Object.assign(payload, { protocol: tcpUdpData.protocol, proxyPort: tcpUdpData.proxyPort - // enableProxy: tcpUdpData.enableProxy }); } const res = await api .put< AxiosResponse - >(`/org/${orgId}/resource/`, payload) + >(`/org/${orgId}/site/${baseData.siteId}/resource/`, payload) .catch((e) => { toast({ variant: "destructive", - title: t("resourceErrorCreate"), + title: "Error creating resource", description: formatAxiosError( e, - t("resourceErrorCreateDescription") + "An error occurred when creating the resource" ) }); }); if (res && res.status === 201) { const id = res.data.data.resourceId; - const niceId = res.data.data.niceId; - setNiceId(niceId); - - // Create targets if any exist - if (targets.length > 0) { - try { - for (const target of targets) { - const data: any = { - ip: target.ip, - port: target.port, - method: target.method, - enabled: target.enabled, - siteId: target.siteId, - 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) { - console.error("Error creating targets:", targetError); - toast({ - variant: "destructive", - title: t("targetErrorCreate"), - description: formatAxiosError( - targetError, - t("targetErrorCreateDescription") - ) - }); - } - } + setResourceId(id); if (isHttp) { - router.push(`/${orgId}/settings/resources/${niceId}`); + router.push(`/${orgId}/settings/resources/${id}`); } else { - const tcpUdpData = tcpUdpForm.getValues(); - // Only show config snippets if enableProxy is explicitly true - // if (tcpUdpData.enableProxy === true) { setShowSnippets(true); router.refresh(); - // } else { - // // If enableProxy is false or undefined, go directly to resource page - // router.push(`/${orgId}/settings/resources/${id}`); - // } } } } catch (e) { - console.error(t("resourceErrorCreateMessage"), e); + console.error("Error creating resource:", e); toast({ variant: "destructive", - title: t("resourceErrorCreate"), - description: t("resourceErrorCreateMessageDescription") + title: "Error creating resource", + description: "An unexpected error occurred" }); } @@ -656,10 +242,10 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t("sitesErrorFetch"), + title: "Error fetching sites", description: formatAxiosError( e, - t("sitesErrorFetchDescription") + "An error occurred when fetching the sites" ) }); }); @@ -667,16 +253,8 @@ export default function Page() { if (res?.status === 200) { setSites(res.data.data.sites); - // Initialize Docker for newt sites - for (const site of res.data.data.sites) { - if (site.type === "newt") { - initializeDockerForSite(site.siteId); - } - } - - // If there's only one site, set it as the default in the form - if (res.data.data.sites.length) { - addTargetForm.setValue( + if (res.data.data.sites.length > 0) { + baseForm.setValue( "siteId", res.data.data.sites[0].siteId ); @@ -692,24 +270,20 @@ export default function Page() { .catch((e) => { toast({ variant: "destructive", - title: t("domainsErrorFetch"), + title: "Error fetching domains", description: formatAxiosError( e, - t("domainsErrorFetchDescription") + "An error occurred when fetching the domains" ) }); }); if (res?.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain) - })); + const domains = res.data.data.domains; setBaseDomains(domains); - // if (domains.length) { - // httpForm.setValue("domainId", domains[0].domainId); - // } + if (domains.length) { + httpForm.setValue("domainId", domains[0].domainId); + } } }; @@ -722,574 +296,12 @@ export default function Page() { load(); }, []); - 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 }) => { - return ( -
- { - const value = parseInt(e.target.value, 10); - if (value >= 1 && value <= 1000) { - updateTarget(row.original.targetId, { - ...row.original, - priority: value - }); - } - }} - /> -
- ); - }, - 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 - ); - - const handleContainerSelectForTarget = ( - hostname: string, - port?: number - ) => { - updateTarget(row.original.targetId, { - ...row.original, - ip: hostname, - ...(port && { port: port }) - }); - }; - - return ( -
-
- {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} - - - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - - updateTarget( - row.original - .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 - }); - } - }} - /> -
-
- ); - }, - size: 400, - minSize: 350, - maxSize: 500 - }; - - 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 - }) - } - /> -
- ), - 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, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { - pagination: { - pageIndex: 0, - pageSize: 1000 - } - } - }); - return ( <>
@@ -1308,18 +320,13 @@ export default function Page() { - {t("resourceInfo")} + Resource Information
{ - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} className="space-y-4" id="base-resource-form" > @@ -1329,7 +336,7 @@ export default function Page() { render={({ field }) => ( - {t("name")} + Name - {t( - "resourceNameDescription" - )} + This is the + display name for + the resource. + + + )} + /> + + ( + + + Site + + + + + + + + + + + + + No + site + found. + + + {sites.map( + ( + site + ) => ( + { + baseForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + + This site will + provide + connectivity to + the resource. )} @@ -1351,92 +451,268 @@ export default function Page() { - {resourceTypes.length > 1 && ( - - - - {t("resourceType")} - - - {t("resourceTypeDescription")} - - - - { - baseForm.setValue( - "http", - value === "http" - ); - // Update method default when switching resource type - addTargetForm.setValue( - "method", - value === "http" - ? "http" - : null - ); - }} - cols={2} - /> - - - )} + + + + Resource Type + + + Determine how you want to access your + resource + + + + { + baseForm.setValue( + "http", + value === "http" + ); + }} + cols={2} + /> + + {baseForm.watch("http") ? ( - {t("resourceHTTPSSettings")} + HTTPS Settings - {t( - "resourceHTTPSSettingsDescription" - )} + Configure how your resource will be + accessed over HTTPS - { - httpForm.setValue( - "subdomain", - res.subdomain - ); - httpForm.setValue( - "domainId", - res.domainId - ); - console.log( - "Domain changed:", - res - ); - }} - /> + + + + {env.flags + .allowBaseDomainResources && ( + ( + + + Domain + Type + + + + + )} + /> + )} + + {!httpForm.watch( + "isBaseDomain" + ) && ( + + + Subdomain + +
+
+ ( + + + + + + + )} + /> +
+
+ ( + + + + + )} + /> +
+
+ + The subdomain + where your + resource will be + accessible. + +
+ )} + + {httpForm.watch( + "isBaseDomain" + ) && ( + ( + + + Base + Domain + + + + + )} + /> + )} + + +
) : ( - {t("resourceRawSettings")} + TCP/UDP Settings - {t( - "resourceRawSettingsDescription" - )} + Configure how your resource will be + accessed over TCP/UDP
{ - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} className="space-y-4" id="tcp-udp-settings-form" > @@ -1448,9 +724,7 @@ export default function Page() { render={({ field }) => ( - {t( - "protocol" - )} + Protocol - {t( - "resourcePortNumberDescription" - )} + The external + port number + to proxy + requests. )} /> - - {/* {build == "oss" && ( - ( - - - - -
- - {t( - "resourceEnableProxy" - )} - - - {t( - "resourceEnableProxyDescription" - )} - -
-
- )} - /> - )} */}
@@ -1573,280 +802,52 @@ export default function Page() {
)} - - - - {t("targets")} - - - {t("targetsDescription")} - - - - {targets.length > 0 ? ( - <> -
- - - {table - .getHeaderGroups() - .map( - ( - headerGroup - ) => ( - - {headerGroup.headers.map( - ( - header - ) => ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ) - )} - - ) - )} - - - {table.getRowModel() - .rows?.length ? ( - table - .getRowModel() - .rows.map( - (row) => ( - - {row - .getVisibleCells() - .map( - ( - cell - ) => ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ) - )} - - ) - ) - ) : ( - - - {t( - "targetNoOne" - )} - - - )} - - {/* */} - {/* {t('targetNoOneDescription')} */} - {/* */} -
-
-
-
- -
- - -
-
-
- - ) : ( -
-

- {t("targetNoOne")} -

- -
- )} -
-
-
- {selectedTargetForHealthCheck && ( - { - if (selectedTargetForHealthCheck) { - console.log(config); - TargetHealthCheck( - selectedTargetForHealthCheck.targetId, - config - ); - } - }} - /> - )} ) : ( - {t("resourceConfig")} + Configuration Snippets - {t("resourceConfigDescription")} + Copy and paste these configuration snippets to set up your TCP/UDP resource

- {t("resourceAddEntrypoints")} + Traefik: Add Entrypoints

-

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

- {t("resourceExposePorts")} + Gerbil: Expose Ports in Docker Compose

-

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

- {t("resourceLearnRaw")} + + Learn how to configure TCP/UDP resources +
@@ -1889,22 +887,20 @@ export default function Page() { type="button" variant="outline" onClick={() => - router.push( - `/${orgId}/settings/resources` - ) + router.push(`/${orgId}/settings/resources`) } > - {t("resourceBack")} + Back to Resources
diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index f4ba9d16..40f6296e 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -1,41 +1,23 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; -import ResourcesTable, { - ResourceRow, - InternalResourceRow -} from "../../../../components/ResourcesTable"; +import ResourcesTable, { ResourceRow } from "./ResourcesTable"; import { AxiosResponse } from "axios"; import { ListResourcesResponse } from "@server/routers/resource"; -import { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { redirect } from "next/navigation"; import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; -import { getTranslations } from "next-intl/server"; -import { pullEnv } from "@app/lib/pullEnv"; -import { toUnicode } from "punycode"; +import ResourcesSplashCard from "./ResourcesSplashCard"; type ResourcesPageProps = { params: Promise<{ orgId: string }>; - searchParams: Promise<{ view?: string }>; }; export const dynamic = "force-dynamic"; export default async function ResourcesPage(props: ResourcesPageProps) { const params = await props.params; - const searchParams = await props.searchParams; - const t = await getTranslations(); - - const env = pullEnv(); - - // Default to 'proxy' view, or use the query param if provided - let defaultView: "proxy" | "internal" = "proxy"; - if (env.flags.enableClients) { - defaultView = searchParams.view === "internal" ? "internal" : "proxy"; - } - let resources: ListResourcesResponse["resources"] = []; try { const res = await internal.get>( @@ -45,14 +27,6 @@ export default async function ResourcesPage(props: ResourcesPageProps) { resources = res.data.data.resources; } catch (e) {} - let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = []; - try { - const res = await internal.get< - AxiosResponse - >(`/org/${params.orgId}/site-resources`, await authCookieHeader()); - siteResources = res.data.data.siteResources; - } catch (e) {} - let org = null; try { const getOrg = cache(async () => @@ -76,8 +50,9 @@ export default async function ResourcesPage(props: ResourcesPageProps) { id: resource.resourceId, name: resource.name, orgId: params.orgId, - nice: resource.niceId, - domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`, + domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, + site: resource.siteName || "None", + siteId: resource.siteId || "Unknown", protocol: resource.protocol, proxyPort: resource.proxyPort, http: resource.http, @@ -86,49 +61,24 @@ export default async function ResourcesPage(props: ResourcesPageProps) { : resource.sso || resource.pincodeId !== null || resource.passwordId !== null || - resource.whitelist || - resource.headerAuthId + resource.whitelist ? "protected" : "not_protected", - enabled: resource.enabled, - domainId: resource.domainId || undefined, - ssl: resource.ssl + enabled: resource.enabled }; }); - const internalResourceRows: InternalResourceRow[] = siteResources.map( - (siteResource) => { - return { - id: siteResource.siteResourceId, - name: siteResource.name, - orgId: params.orgId, - siteName: siteResource.siteName, - protocol: siteResource.protocol, - proxyPort: siteResource.proxyPort, - siteId: siteResource.siteId, - destinationIp: siteResource.destinationIp, - destinationPort: siteResource.destinationPort, - siteNiceId: siteResource.siteNiceId - }; - } - ); - return ( <> + {/* */} + - + ); diff --git a/src/components/AccessTokenUsage.tsx b/src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx similarity index 72% rename from src/components/AccessTokenUsage.tsx rename to src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx index c44f43b7..5f44ca52 100644 --- a/src/components/AccessTokenUsage.tsx +++ b/src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx @@ -15,7 +15,6 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { useEnvContext } from "@app/hooks/useEnvContext"; import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyTextBox from "@app/components/CopyTextBox"; -import { useTranslations } from "next-intl"; interface AccessTokenSectionProps { token: string; @@ -38,37 +37,37 @@ export default function AccessTokenSection({ setTimeout(() => setCopied(null), 2000); }; - const t = useTranslations(); - return ( <>

- {t('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.

- {t('accessToken')} - {t('usageExamples')} + Access Token + Usage Examples
-
{t('tokenId')}
+
Token ID
-
{t('token')}
+
Token
-

{t('requestHeades')}

+

Request Headers

-

{t('queryParameter')}

+

Query Parameter

@@ -85,17 +84,21 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`} - {t('importantNote')} + Important Note - {t('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.
- {t('shareTokenSecurety')} + Keep your access token secure. Do not share it in publicly + accessible areas or client-side code.
); diff --git a/src/components/CreateShareLinkForm.tsx b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx similarity index 89% rename from src/components/CreateShareLinkForm.tsx rename to src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx index b38bab91..871f0ca0 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx @@ -58,16 +58,14 @@ import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { Checkbox } from "@app/components/ui/checkbox"; import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; import { constructShareLink } from "@app/lib/shareLinks"; -import { ShareLinkRow } from "@app/components/ShareLinksTable"; +import { ShareLinkRow } from "./ShareLinksTable"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; -import AccessTokenSection from "@app/components/AccessTokenUsage"; -import { useTranslations } from "next-intl"; -import { toUnicode } from 'punycode'; +import AccessTokenSection from "./AccessTokenUsage"; type FormProps = { open: boolean; @@ -75,6 +73,15 @@ type FormProps = { onCreated?: (result: ShareLinkRow) => void; }; +const formSchema = z.object({ + resourceId: z.number({ message: "Please select a resource" }), + resourceName: z.string(), + resourceUrl: z.string(), + timeUnit: z.string(), + timeValue: z.coerce.number().int().positive().min(1), + title: z.string().optional() +}); + export default function CreateShareLinkForm({ open, setOpen, @@ -92,32 +99,23 @@ export default function CreateShareLinkForm({ const [neverExpire, setNeverExpire] = useState(false); const [isOpen, setIsOpen] = useState(false); - const t = useTranslations(); const [resources, setResources] = useState< { resourceId: number; name: string; resourceUrl: string; + siteName: string | null; }[] >([]); - const formSchema = z.object({ - resourceId: z.number({ message: t('shareErrorSelectResource') }), - resourceName: z.string(), - resourceUrl: z.string(), - timeUnit: z.string(), - timeValue: z.coerce.number().int().positive().min(1), - title: z.string().optional() - }); - const timeUnits = [ - { unit: "minutes", name: t('minutes') }, - { unit: "hours", name: t('hours') }, - { unit: "days", name: t('days') }, - { unit: "weeks", name: t('weeks') }, - { unit: "months", name: t('months') }, - { unit: "years", name: t('years') } + { unit: "minutes", name: "Minutes" }, + { unit: "hours", name: "Hours" }, + { unit: "days", name: "Days" }, + { unit: "weeks", name: "Weeks" }, + { unit: "months", name: "Months" }, + { unit: "years", name: "Years" } ]; const form = useForm>({ @@ -143,10 +141,10 @@ export default function CreateShareLinkForm({ console.error(e); toast({ variant: "destructive", - title: t('shareErrorFetchResource'), + title: "Failed to fetch resources", description: formatAxiosError( e, - t('shareErrorFetchResourceDescription') + "An error occurred while fetching the resources" ) }); }); @@ -160,7 +158,8 @@ export default function CreateShareLinkForm({ .map((r) => ({ resourceId: r.resourceId, name: r.name, - resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`, + siteName: r.siteName })) ); } @@ -202,17 +201,17 @@ export default function CreateShareLinkForm({ validForSeconds: neverExpire ? undefined : timeInSeconds, title: values.title || - t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)}) + `${values.resourceName || "Resource" + values.resourceId} Share Link` } ) .catch((e) => { console.error(e); toast({ variant: "destructive", - title: t('shareErrorCreate'), + title: "Failed to create share link", description: formatAxiosError( e, - t('shareErrorCreateDescription') + "An error occurred while creating the share link" ) }); }); @@ -235,7 +234,8 @@ export default function CreateShareLinkForm({ resourceName: values.resourceName, title: token.title, createdAt: token.createdAt, - expiresAt: token.expiresAt + expiresAt: token.expiresAt, + siteName: resource?.siteName || null }); } @@ -244,7 +244,7 @@ export default function CreateShareLinkForm({ function getSelectedResourceName(id: number) { const resource = resources.find((r) => r.resourceId === id); - return `${resource?.name}`; + return `${resource?.name} ${resource?.siteName ? `(${resource.siteName})` : ""}`; } return ( @@ -260,9 +260,9 @@ export default function CreateShareLinkForm({ > - {t('shareCreate')} + Create Shareable Link - {t('shareCreateDescription')} + Anyone with this link can access the resource @@ -280,7 +280,7 @@ export default function CreateShareLinkForm({ render={({ field }) => ( - {t('resource')} + Resource @@ -298,17 +298,19 @@ export default function CreateShareLinkForm({ ? getSelectedResourceName( field.value ) - : t('resourceSelect')} + : "Select resource"} - + - {t('resourcesNotFound')} + No + resources + found {resources.map( @@ -344,7 +346,7 @@ export default function CreateShareLinkForm({ : "opacity-0" )} /> - {`${r.name}`} + {`${r.name} ${r.siteName ? `(${r.siteName})` : ""}`} ) )} @@ -364,7 +366,7 @@ export default function CreateShareLinkForm({ render={({ field }) => ( - {t('shareTitleOptional')} + Title (optional) @@ -376,7 +378,7 @@ export default function CreateShareLinkForm({
- {t('expireIn')} + Expire In
- - + + @@ -424,7 +425,6 @@ export default function CreateShareLinkForm({ ( @@ -455,12 +455,18 @@ export default function CreateShareLinkForm({ htmlFor="terms" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > - {t('neverExpire')} + Never expire

- {t('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.

@@ -469,16 +475,16 @@ export default function CreateShareLinkForm({ {link && (

- {t('shareSeeOnce')} + You will only be able to see this link + once. Make sure to copy it.

- {t('shareAccessHint')} + Anyone with this link can access the + resource. Share it with care.

-
-

- {t('shareTokenUsage')} + See Access Token Usage

- {t('toggle')} + Toggle
@@ -535,7 +541,7 @@ export default function CreateShareLinkForm({ - + diff --git a/src/components/ShareLinksDataTable.tsx b/src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx similarity index 55% rename from src/components/ShareLinksDataTable.tsx rename to src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx index f2753bcf..35ab6d3d 100644 --- a/src/components/ShareLinksDataTable.tsx +++ b/src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx @@ -4,38 +4,27 @@ 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[]; createShareLink?: () => void; - onRefresh?: () => void; - isRefreshing?: boolean; } export function ShareLinksDataTable({ columns, data, - createShareLink, - onRefresh, - isRefreshing + createShareLink }: DataTableProps) { - - const t = useTranslations(); - return ( ); } diff --git a/src/components/ShareLinksSplash.tsx b/src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx similarity index 80% rename from src/components/ShareLinksSplash.tsx rename to src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx index 8bba8787..decaafdf 100644 --- a/src/components/ShareLinksSplash.tsx +++ b/src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx @@ -4,7 +4,6 @@ import React, { useState, useEffect } from "react"; import { Link, X, Clock, Share, ArrowRight, Lock } from "lucide-react"; // Replace with actual imports import { Card, CardContent } from "@app/components/ui/card"; import { Button } from "@app/components/ui/button"; -import { useTranslations } from "next-intl"; export const ShareableLinksSplash = () => { const [isDismissed, setIsDismissed] = useState(false); @@ -23,8 +22,6 @@ export const ShareableLinksSplash = () => { localStorage.setItem(key, "true"); }; - const t = useTranslations(); - if (isDismissed) { return null; } @@ -34,7 +31,7 @@ export const ShareableLinksSplash = () => { @@ -42,23 +39,26 @@ export const ShareableLinksSplash = () => {

- {t('share')} + Shareable Links

- {t('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.

  • - {t('shareEasyCreate')} + Easy to create and share
  • - {t('shareConfigurableExpirationDuration')} + Configurable expiration duration
  • - {t('shareSecureAndRevocable')} + Secure and revocable
diff --git a/src/components/ShareLinksTable.tsx b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx similarity index 72% rename from src/components/ShareLinksTable.tsx rename to src/app/[orgId]/settings/share-links/ShareLinksTable.tsx index ba9169c1..69c88cf7 100644 --- a/src/components/ShareLinksTable.tsx +++ b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { ShareLinksDataTable } from "@app/components/ShareLinksDataTable"; +import { ShareLinksDataTable } from "./ShareLinksDataTable"; import { DropdownMenu, DropdownMenuContent, @@ -31,9 +31,8 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { ArrayElement } from "@server/types/ArrayElement"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; import moment from "moment"; -import CreateShareLinkForm from "@app/components/CreateShareLinkForm"; +import CreateShareLinkForm from "./CreateShareLinkForm"; import { constructShareLink } from "@app/lib/shareLinks"; -import { useTranslations } from "next-intl"; export type ShareLinkRow = { accessTokenId: string; @@ -42,6 +41,7 @@ export type ShareLinkRow = { title: string | null; createdAt: number; expiresAt: number | null; + siteName: string | null; }; type ShareLinksTableProps = { @@ -54,32 +54,12 @@ export default function ShareLinksTable({ orgId }: ShareLinksTableProps) { const router = useRouter(); - const t = useTranslations(); const api = createApiClient(useEnvContext()); 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); } @@ -87,8 +67,11 @@ export default function ShareLinksTable({ async function deleteSharelink(id: string) { await api.delete(`/access-token/${id}`).catch((e) => { toast({ - title: t("shareErrorDelete"), - description: formatAxiosError(e, t("shareErrorDeleteMessage")) + title: "Failed to delete link", + description: formatAxiosError( + e, + "An error occurred deleting link" + ) }); }); @@ -96,12 +79,53 @@ export default function ShareLinksTable({ setRows(newRows); toast({ - title: t("shareDeleted"), - description: t("shareDeletedDescription") + title: "Link deleted", + description: "The link has been deleted" }); } const columns: ColumnDef[] = [ + { + id: "actions", + cell: ({ row }) => { + const router = useRouter(); + + const resourceRow = row.original; + + return ( + <> +
+ + + + + + { + deleteSharelink( + resourceRow.accessTokenId + ); + }} + > + + + + +
+ + ); + } + }, { accessorKey: "resourceName", header: ({ column }) => { @@ -112,7 +136,7 @@ export default function ShareLinksTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t("resource")} + Resource ); @@ -121,8 +145,9 @@ export default function ShareLinksTable({ const r = row.original; return ( - @@ -139,7 +164,7 @@ export default function ShareLinksTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t("title")} + Title ); @@ -218,7 +243,7 @@ export default function ShareLinksTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t("created")} + Created ); @@ -238,7 +263,7 @@ export default function ShareLinksTable({ column.toggleSorting(column.getIsSorted() === "asc") } > - {t("expires")} + Expires ); @@ -248,50 +273,23 @@ export default function ShareLinksTable({ if (r.expiresAt) { return moment(r.expiresAt).format("lll"); } - return t("never"); + return "Never"; } }, { id: "delete", - cell: ({ row }) => { - const resourceRow = row.original; - return ( -
- {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* { */} - {/* deleteSharelink( */} - {/* resourceRow.accessTokenId */} - {/* ); */} - {/* }} */} - {/* > */} - {/* */} - {/* */} - {/* */} - {/* */} - -
- ); - } + cell: ({ row }) => ( +
+ +
+ ) } ]; @@ -311,8 +309,6 @@ export default function ShareLinksTable({ createShareLink={() => { setIsCreateModalOpen(true); }} - onRefresh={refreshData} - isRefreshing={isRefreshing} /> ); diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index caf02b83..0bfa023d 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -7,8 +7,8 @@ import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; -import ShareLinksTable, { ShareLinkRow } from "../../../../components/ShareLinksTable"; -import { getTranslations } from "next-intl/server"; +import ShareLinksTable, { ShareLinkRow } from "./ShareLinksTable"; +import ShareableLinksSplash from "./ShareLinksSplash"; type ShareLinksPageProps = { params: Promise<{ orgId: string }>; @@ -51,15 +51,13 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) { (token) => ({ ...token }) as ShareLinkRow ); - const t = await getTranslations(); - return ( <> {/* */} diff --git a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx new file mode 100644 index 00000000..c4da2336 --- /dev/null +++ b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx @@ -0,0 +1,461 @@ +"use client"; + +import { + Form, + FormControl, + FormDescription, + 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 { useParams, useRouter } from "next/navigation"; +import { + CreateSiteBody, + CreateSiteResponse, + PickSiteDefaultsResponse +} from "@server/routers/site"; +import { generateKeypair } from "./[niceId]/wireguardConfig"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { Checkbox } from "@app/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { SiteRow } from "./SitesTable"; +import { AxiosResponse } from "axios"; +import { Button } from "@app/components/ui/button"; +import Link from "next/link"; +import { + ArrowUpRight, + ChevronsUpDown, + Loader2, + SquareArrowOutUpRight +} from "lucide-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; +import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; + +const createSiteFormSchema = z.object({ + name: z + .string() + .min(2, { + message: "Name must be at least 2 characters." + }) + .max(30, { + message: "Name must not be longer than 30 characters." + }), + method: z.enum(["wireguard", "newt", "local"]) +}); + +type CreateSiteFormValues = z.infer; + +const defaultValues: Partial = { + name: "", + method: "newt" +}; + +type CreateSiteFormProps = { + onCreate?: (site: SiteRow) => void; + setLoading?: (loading: boolean) => void; + setChecked?: (checked: boolean) => void; + orgId: string; +}; + +export default function CreateSiteForm({ + onCreate, + setLoading, + setChecked, + orgId +}: CreateSiteFormProps) { + const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + + const [isLoading, setIsLoading] = useState(false); + const [isChecked, setIsChecked] = useState(false); + + const [isOpen, setIsOpen] = useState(false); + + const [keypair, setKeypair] = useState<{ + publicKey: string; + privateKey: string; + } | null>(null); + + const [siteDefaults, setSiteDefaults] = + useState(null); + + const [loadingPage, setLoadingPage] = useState(true); + + const handleCheckboxChange = (checked: boolean) => { + // setChecked?.(checked); + setIsChecked(checked); + }; + + const form = useForm({ + resolver: zodResolver(createSiteFormSchema), + defaultValues + }); + + const nameField = form.watch("name"); + const methodField = form.watch("method"); + + useEffect(() => { + const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30; + const isFormValid = methodField === "local" || isChecked; + + // Only set checked to true if name is valid AND (method is local OR checkbox is checked) + setChecked?.(nameIsValid && isFormValid); + }, [nameField, methodField, isChecked, setChecked]); + + useEffect(() => { + if (!open) return; + + const load = async () => { + setLoadingPage(true); + // reset all values + setLoading?.(false); + setIsLoading(false); + form.reset(); + setChecked?.(false); + setKeypair(null); + setSiteDefaults(null); + + const generatedKeypair = generateKeypair(); + setKeypair(generatedKeypair); + + 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"); + }) + .then((res) => { + if (res && res.status === 200) { + setSiteDefaults(res.data.data); + } + }); + await new Promise((resolve) => setTimeout(resolve, 200)); + + setLoadingPage(false); + }; + + load(); + }, [open]); + + async function onSubmit(data: CreateSiteFormValues) { + setLoading?.(true); + setIsLoading(true); + let payload: CreateSiteBody = { + name: data.name, + type: data.method + }; + + if (data.method == "wireguard") { + if (!keypair || !siteDefaults) { + toast({ + variant: "destructive", + title: "Error creating site", + description: "Key pair or site defaults not found" + }); + setLoading?.(false); + setIsLoading(false); + return; + } + + payload = { + ...payload, + subnet: siteDefaults.subnet, + exitNodeId: siteDefaults.exitNodeId, + pubKey: keypair.publicKey + }; + } + if (data.method === "newt") { + if (!siteDefaults) { + toast({ + variant: "destructive", + title: "Error creating site", + description: "Site defaults not found" + }); + setLoading?.(false); + setIsLoading(false); + return; + } + + payload = { + ...payload, + subnet: siteDefaults.subnet, + exitNodeId: siteDefaults.exitNodeId, + secret: siteDefaults.newtSecret, + newtId: siteDefaults.newtId + }; + } + + const res = await api + .put< + AxiosResponse + >(`/org/${orgId}/site/`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error creating site", + description: formatAxiosError(e) + }); + }); + + if (res && res.status === 201) { + const data = res.data.data; + + onCreate?.({ + name: data.name, + id: data.siteId, + nice: data.niceId.toString(), + mbIn: + data.type == "wireguard" || data.type == "newt" + ? "0 MB" + : "-", + mbOut: + data.type == "wireguard" || data.type == "newt" + ? "0 MB" + : "-", + orgId: orgId as string, + type: data.type as any, + online: false + }); + } + + setLoading?.(false); + setIsLoading(false); + } + + const wgConfig = + keypair && siteDefaults + ? `[Interface] +Address = ${siteDefaults.subnet} +ListenPort = 51820 +PrivateKey = ${keypair.privateKey} + +[Peer] +PublicKey = ${siteDefaults.publicKey} +AllowedIPs = ${siteDefaults.address.split("/")[0]}/32 +Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort} +PersistentKeepalive = 5` + : ""; + + const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; + + const newtConfigDockerCompose = `services: + newt: + image: fosrl/newt + container_name: newt + restart: unless-stopped + environment: + - PANGOLIN_ENDPOINT=${env.app.dashboardUrl} + - NEWT_ID=${siteDefaults?.newtId} + - NEWT_SECRET=${siteDefaults?.newtSecret}`; + + const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; + + return loadingPage ? ( + + ) : ( +
+
+ + ( + + Name + + + + + + This is the display name for the site. + + + )} + /> + ( + + Method + + + + + + This is how you will expose connections. + + + )} + /> + + {form.watch("method") === "newt" && ( + + + Learn how to install Newt on your system + + + + )} + +
+ {form.watch("method") === "wireguard" && !isLoading ? ( + <> + + + You will only be able to see the + configuration once. + + + ) : form.watch("method") === "wireguard" && + isLoading ? ( +

Loading WireGuard configuration...

+ ) : form.watch("method") === "newt" && siteDefaults ? ( + <> +
+ +
+ +
+ + You will only be able to see the + configuration once. + +
+ + + +
+ +
+ Docker Compose + +
+
+ Docker Run + + +
+
+
+
+ + ) : null} +
+ + {form.watch("method") === "local" && ( + + Local sites do not tunnel, learn more + + + )} + + {(form.watch("method") === "newt" || + form.watch("method") === "wireguard") && ( +
+ + +
+ )} + + +
+ ); +} diff --git a/src/app/[orgId]/settings/sites/CreateSiteModal.tsx b/src/app/[orgId]/settings/sites/CreateSiteModal.tsx new file mode 100644 index 00000000..1666000d --- /dev/null +++ b/src/app/[orgId]/settings/sites/CreateSiteModal.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { SiteRow } from "./SitesTable"; +import CreateSiteForm from "./CreateSiteForm"; + +type CreateSiteFormProps = { + open: boolean; + setOpen: (open: boolean) => void; + onCreate?: (site: SiteRow) => void; + orgId: string; +}; + +export default function CreateSiteFormModal({ + open, + setOpen, + onCreate, + orgId +}: CreateSiteFormProps) { + const [loading, setLoading] = useState(false); + const [isChecked, setIsChecked] = useState(false); + + return ( + <> + { + setOpen(val); + setLoading(false); + }} + > + + + Create Site + + Create a new site to start connecting your resources + + + +
+ setLoading(val)} + setChecked={(val) => setIsChecked(val)} + onCreate={onCreate} + orgId={orgId} + /> +
+
+ + + + + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/sites/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/SitesDataTable.tsx new file mode 100644 index 00000000..08d97955 --- /dev/null +++ b/src/app/[orgId]/settings/sites/SitesDataTable.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { + ColumnDef, +} from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + createSite?: () => void; +} + +export function SitesDataTable({ + columns, + data, + createSite +}: DataTableProps) { + return ( + + ); +} diff --git a/src/components/SitesSplashCard.tsx b/src/app/[orgId]/settings/sites/SitesSplashCard.tsx similarity index 77% rename from src/components/SitesSplashCard.tsx rename to src/app/[orgId]/settings/sites/SitesSplashCard.tsx index 35d7bd83..6734e66b 100644 --- a/src/components/SitesSplashCard.tsx +++ b/src/app/[orgId]/settings/sites/SitesSplashCard.tsx @@ -5,15 +5,11 @@ import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ArrowRight, DockIcon as Docker, Globe, Server, X } from "lucide-react"; import Link from "next/link"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from 'next-intl'; export const SitesSplashCard = () => { const [isDismissed, setIsDismissed] = useState(true); - const { env } = useEnvContext(); const key = "sites-splash-card-dismissed"; - const t = useTranslations(); useEffect(() => { const dismissed = localStorage.getItem(key); @@ -38,7 +34,7 @@ export const SitesSplashCard = () => { @@ -46,25 +42,28 @@ export const SitesSplashCard = () => {

- Newt ({t('recommended')}) + Newt (Recommended)

- {t('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.

  • - {t('siteRunsInDocker')} + Runs in Docker
  • - {t('siteRunsInShell')} + Runs in shell on macOS, Linux, and Windows
@@ -72,7 +71,7 @@ export const SitesSplashCard = () => { className="w-full flex items-center" variant="secondary" > - {t('siteInstallNewt')}{" "} + Install Newt{" "} @@ -80,19 +79,20 @@ export const SitesSplashCard = () => {

- {t('siteWg')} + Basic WireGuard

- {t('siteWgAnyClients')} + Use any WireGuard client to connect. You will have to + address your internal resources using the peer IP.

  • - {t('siteWgCompatibleAllClients')} + Compatible with all WireGuard clients
  • - {t('siteWgManualConfigurationRequired')} + Manual configuration required
diff --git a/src/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/SitesTable.tsx similarity index 51% rename from src/components/SitesTable.tsx rename to src/app/[orgId]/settings/sites/SitesTable.tsx index 851514ea..c032800f 100644 --- a/src/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesTable.tsx @@ -1,7 +1,7 @@ "use client"; -import { Column, ColumnDef } from "@tanstack/react-table"; -import { SitesDataTable } from "@app/components/SitesDataTable"; +import { ColumnDef } from "@tanstack/react-table"; +import { SitesDataTable } from "./SitesDataTable"; import { DropdownMenu, DropdownMenuContent, @@ -19,16 +19,14 @@ import { import Link from "next/link"; import { useRouter } from "next/navigation"; import { AxiosResponse } from "axios"; -import { useState, useEffect } from "react"; +import { useState } from "react"; +import CreateSiteForm from "./CreateSiteForm"; 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 { useTranslations } from "next-intl"; -import { parseDataSize } from "@app/lib/dataSize"; -import { Badge } from "@app/components/ui/badge"; -import { InfoPopup } from "@app/components/ui/info-popup"; +import CreateSiteFormModal from "./CreateSiteModal"; export type SiteRow = { id: number; @@ -38,12 +36,7 @@ export type SiteRow = { mbOut: string; orgId: string; type: "newt" | "wireguard"; - newtVersion?: string; - newtUpdateAvailable?: boolean; online: boolean; - address?: string; - exitNodeName?: string; - exitNodeEndpoint?: string; }; type SitesTableProps = { @@ -57,42 +50,17 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedSite, setSelectedSite] = useState(null); const [rows, setRows] = useState(sites); - const [isRefreshing, setIsRefreshing] = useState(false); const api = createApiClient(useEnvContext()); - const t = useTranslations(); - const { env } = useEnvContext(); - - // Update local state when props change (e.g., after refresh) - useEffect(() => { - setRows(sites); - }, [sites]); - - 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 = (siteId: number) => { api.delete(`/site/${siteId}`) .catch((e) => { - console.error(t("siteErrorDelete"), e); + console.error("Error deleting site", e); toast({ variant: "destructive", - title: t("siteErrorDelete"), - description: formatAxiosError(e, t("siteErrorDelete")) + title: "Error deleting site", + description: formatAxiosError(e, "Error deleting site") }); }) .then(() => { @@ -106,6 +74,42 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { }; const columns: ColumnDef[] = [ + { + id: "dots", + cell: ({ row }) => { + const siteRow = row.original; + const router = useRouter(); + + return ( + + + + + + + + View settings + + + { + setSelectedSite(siteRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + + ); + } + }, { accessorKey: "name", header: ({ column }) => { @@ -116,7 +120,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("name")} + Name ); @@ -132,7 +136,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("online")} + Online ); @@ -147,14 +151,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { return (
- {t("online")} + Online
); } else { return (
- {t("offline")} + Offline
); } @@ -172,19 +176,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { onClick={() => column.toggleSorting(column.getIsSorted() === "asc") } - className="hidden md:flex whitespace-nowrap" > - {t("site")} + Site ); - }, - cell: ({ row }) => { - return ( -
- {row.original.nice} -
- ); } }, { @@ -197,14 +193,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("dataIn")} + Data In ); - }, - sortingFn: (rowA, rowB) => - parseDataSize(rowA.original.mbIn) - - parseDataSize(rowB.original.mbIn) + } }, { accessorKey: "mbOut", @@ -216,14 +209,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("dataOut")} + Data Out ); - }, - sortingFn: (rowA, rowB) => - parseDataSize(rowA.original.mbOut) - - parseDataSize(rowB.original.mbOut) + } }, { accessorKey: "type", @@ -235,7 +225,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("connectionType")} + Connection Type ); @@ -245,22 +235,8 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { if (originalRow.type === "newt") { return ( -
- -
- Newt - {originalRow.newtVersion && ( - - v{originalRow.newtVersion} - - )} -
-
- {originalRow.newtUpdateAvailable && ( - - )} +
+ Newt
); } @@ -276,114 +252,23 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { if (originalRow.type === "local") { return (
- {t("local")} + Local
); } } }, - { - 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 }) => { const siteRow = row.original; return ( -
- - - - - - - - {t("viewSettings")} - - - { - setSelectedSite(siteRow); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - - - - +
- @@ -405,21 +290,30 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { dialog={

- {t("siteQuestionRemove", { - selectedSite: - selectedSite?.name || selectedSite?.id - })} + Are you sure you want to remove the site{" "} + {selectedSite?.name || selectedSite?.id}{" "} + from the organization?

-

{t("siteMessageRemove")}

+

+ Once removed, the site will no longer be + accessible.{" "} + + All resources and targets associated with + the site will also be removed. + +

-

{t("siteMessageConfirm")}

+

+ To confirm, please type the name of the site + below. +

} - buttonText={t("siteConfirmDelete")} + buttonText="Confirm Delete Site" onConfirm={async () => deleteSite(selectedSite!.id)} string={selectedSite.name} - title={t("siteDelete")} + title="Delete Site" /> )} @@ -429,8 +323,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { createSite={() => router.push(`/${orgId}/settings/sites/create`) } - onRefresh={refreshData} - isRefreshing={isRefreshing} /> ); diff --git a/src/components/SiteInfoCard.tsx b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx similarity index 63% rename from src/components/SiteInfoCard.tsx rename to src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx index 5eed91c5..ee4758be 100644 --- a/src/components/SiteInfoCard.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx @@ -9,15 +9,11 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { useTranslations } from "next-intl"; -import { useEnvContext } from "@app/hooks/useEnvContext"; type SiteInfoCardProps = {}; export default function SiteInfoCard({}: SiteInfoCardProps) { const { site, updateSite } = useSiteContext(); - const t = useTranslations(); - const { env } = useEnvContext(); const getConnectionTypeString = (type: string) => { if (type === "newt") { @@ -25,32 +21,32 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { } else if (type === "wireguard") { return "WireGuard"; } else if (type === "local") { - return t("local"); + return "Local"; } else { - return t("unknown"); + return "Unknown"; } }; return ( - - + + Site Information + + {(site.type == "newt" || site.type == "wireguard") && ( <> - - {t("status")} - + Status {site.online ? (
- {t("online")} + Online
) : (
- {t("offline")} + Offline
)}
@@ -58,22 +54,11 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { )} - - {t("connectionType")} - + Connection Type {getConnectionTypeString(site.type)} - - {env.flags.enableClients && site.type == "newt" && ( - - Address - - {site.address?.split("/")[0]} - - - )}
diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 432d4bd3..f107d960 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -24,28 +24,16 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm + SettingsSectionForm, + SettingsSectionFooter } from "@app/components/Settings"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useState } from "react"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { useTranslations } from "next-intl"; -import Link from "next/link"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; const GeneralFormSchema = z.object({ - name: z.string().nonempty("Name is required"), - dockerSocketEnabled: z.boolean().optional(), - remoteSubnets: z - .array( - z.object({ - id: z.string(), - text: z.string() - }) - ) - .optional() + name: z.string().nonempty("Name is required") }); type GeneralFormValues = z.infer; @@ -53,28 +41,16 @@ type GeneralFormValues = z.infer; export default function GeneralPage() { const { site, updateSite } = useSiteContext(); - const { env } = useEnvContext(); const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); - const [activeCidrTagIndex, setActiveCidrTagIndex] = useState( - null - ); const router = useRouter(); - const t = useTranslations(); - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { - name: site?.name, - dockerSocketEnabled: site?.dockerSocketEnabled ?? false, - remoteSubnets: site?.remoteSubnets - ? site.remoteSubnets.split(",").map((subnet, index) => ({ - id: subnet.trim(), - text: subnet.trim() - })) - : [] + name: site?.name }, mode: "onChange" }); @@ -84,34 +60,24 @@ export default function GeneralPage() { await api .post(`/site/${site?.siteId}`, { - name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: - data.remoteSubnets - ?.map((subnet) => subnet.text) - .join(",") || "" + name: data.name }) .catch((e) => { toast({ variant: "destructive", - title: t("siteErrorUpdate"), + title: "Failed to update site", description: formatAxiosError( e, - t("siteErrorUpdateDescription") + "An error occurred while updating the site." ) }); }); - updateSite({ - name: data.name, - dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: - data.remoteSubnets?.map((subnet) => subnet.text).join(",") || "" - }); + updateSite({ name: data.name }); toast({ - title: t("siteUpdated"), - description: t("siteUpdatedDescription") + title: "Site updated", + description: "The site has been updated." }); setLoading(false); @@ -124,10 +90,10 @@ export default function GeneralPage() { - {t("generalSettings")} + General Settings - {t("siteGeneralDescription")} + Configure the general settings for this site @@ -136,7 +102,7 @@ export default function GeneralPage() {
( - {t("name")} + Name + + This is the display name of the + site. + )} /> - - {env.flags.enableClients && - site.type === "newt" ? ( - ( - - - {t("remoteSubnets")} - - - { - form.setValue( - "remoteSubnets", - newSubnets as Tag[] - ); - }} - validateTag={(tag) => { - // Basic CIDR validation regex - const cidrRegex = - /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; - return cidrRegex.test( - tag - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t( - "remoteSubnetsDescription" - )} - - - - )} - /> - ) : null} - - {site && site.type === "newt" && ( - ( - - - - - - - {t( - "enableDockerSocketDescription" - )}{" "} - - - {t( - "enableDockerSocketLink" - )} - - - - - )} - /> - )} -
-
- -
+ + + + ); } diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index 039deebb..5bcc8af9 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -5,9 +5,17 @@ import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SiteInfoCard from "../../../../../components/SiteInfoCard"; -import { getTranslations } from "next-intl/server"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import SiteInfoCard from "./SiteInfoCard"; interface SettingsLayoutProps { children: React.ReactNode; @@ -30,11 +38,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/sites`); } - const t = await getTranslations(); - const navItems = [ { - title: t('general'), + title: "General", href: "/{orgId}/settings/sites/{niceId}/general" } ]; @@ -42,8 +48,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 60fe0c7d..38c8a772 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -21,7 +21,7 @@ import { } from "@app/components/ui/form"; import HeaderTitle from "@app/components/SettingsSectionTitle"; import { z } from "zod"; -import { createElement, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; @@ -42,8 +42,7 @@ import { FaFreebsd, FaWindows } from "react-icons/fa"; -import { SiNixos, SiKubernetes } from "react-icons/si"; -import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -53,13 +52,44 @@ 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 { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import Link from "next/link"; import { QRCodeCanvas } from "qrcode.react"; -import { useTranslations } from "next-intl"; +const createSiteFormSchema = z + .object({ + name: z + .string() + .min(2, { message: "Name must be at least 2 characters." }) + .max(30, { + message: "Name must not be longer than 30 characters." + }), + method: z.enum(["newt", "wireguard", "local"]), + copied: z.boolean() + }) + .refine( + (data) => { + if (data.method !== "local") { + return data.copied; + } + return true; + }, + { + message: "Please confirm that you have copied the config.", + path: ["copied"] + } + ); + +type CreateSiteFormValues = z.infer; type SiteType = "newt" | "wireguard" | "local"; @@ -70,33 +100,21 @@ interface TunnelTypeOption { disabled?: boolean; } -interface RemoteExitNodeOption { - id: string; - title: string; - description: string; - disabled?: boolean; -} - type Commands = { mac: Record; linux: Record; - freebsd: Record; windows: Record; docker: Record; - kubernetes: Record; podman: Record; - nixos: Record; }; const platforms = [ "linux", "docker", - "kubernetes", "podman", "mac", "windows", - "freebsd", - "nixos" + "freebsd" ] as const; type Platform = (typeof platforms)[number]; @@ -106,53 +124,29 @@ export default function Page() { const api = createApiClient({ env }); const { orgId } = useParams(); const router = useRouter(); - const t = useTranslations(); - - const createSiteFormSchema = z - .object({ - name: z - .string() - .min(2, { message: t("nameMin", { len: 2 }) }) - .max(30, { - message: t("nameMax", { len: 30 }) - }), - method: z.enum(["newt", "wireguard", "local"]), - copied: z.boolean(), - clientAddress: z.string().optional(), - acceptClients: z.boolean(), - exitNodeId: z.number().optional() - }); - - type CreateSiteFormValues = z.infer; const [tunnelTypes, setTunnelTypes] = useState< ReadonlyArray >([ { id: "newt", - title: t("siteNewtTunnel"), - description: t("siteNewtTunnelDescription"), + title: "Newt Tunnel (Recommended)", + description: + "Easiest way to create an entrypoint into your network. No extra setup.", disabled: true }, - ...(env.flags.disableBasicWireguardSites - ? [] - : [ - { - id: "wireguard" as SiteType, - title: t("siteWg"), - description: t("siteWgDescription"), - disabled: true - } - ]), - ...(env.flags.disableLocalSites - ? [] - : [ - { - id: "local" as SiteType, - title: t("local"), - description: t("siteLocalDescription") - } - ]) + { + id: "wireguard", + title: "Basic WireGuard", + description: + "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", + disabled: true + }, + { + id: "local", + title: "Local", + description: "Local resources only. No tunneling." + } ]); const [loadingPage, setLoadingPage] = useState(true); @@ -164,25 +158,16 @@ export default function Page() { const [newtId, setNewtId] = useState(""); const [newtSecret, setNewtSecret] = useState(""); const [newtEndpoint, setNewtEndpoint] = useState(""); - const [clientAddress, setClientAddress] = useState(""); + const [publicKey, setPublicKey] = useState(""); const [privateKey, setPrivateKey] = useState(""); const [wgConfig, setWgConfig] = useState(""); const [createLoading, setCreateLoading] = useState(false); - const [acceptClients, setAcceptClients] = useState(false); - const [newtVersion, setNewtVersion] = useState("latest"); const [siteDefaults, setSiteDefaults] = useState(null); - const [remoteExitNodeOptions, setRemoteExitNodeOptions] = useState< - ReadonlyArray - >([]); - const [selectedExitNodeId, setSelectedExitNodeId] = useState< - string | undefined - >(); - const hydrateWireGuardConfig = ( privateKey: string, publicKey: string, @@ -208,61 +193,55 @@ PersistentKeepalive = 5`; id: string, secret: string, endpoint: string, - version: string, - acceptClients: boolean = false + version: string ) => { - const acceptClientsFlag = acceptClients ? " --accept-clients" : ""; - const acceptClientsEnv = acceptClients - ? "\n - ACCEPT_CLIENTS=true" - : ""; - const commands = { mac: { - All: [ - `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + "Apple Silicon (arm64)": [ + `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_arm64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + "Intel x64 (amd64)": [ + `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_amd64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] - // "Intel x64 (amd64)": [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ] }, linux: { - All: [ - `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + amd64: [ + `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_amd64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm64: [ + `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm32: [ + `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm32v6: [ + `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32v6" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + riscv64: [ + `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_riscv64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] - // arm64: [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ], - // arm32: [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ], - // arm32v6: [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ], - // riscv64: [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ] }, freebsd: { - All: [ - `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + amd64: [ + `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_amd64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` + ], + arm64: [ + `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_arm64" && chmod +x ./newt`, + `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] - // arm64: [ - // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, - // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - // ] }, windows: { x64: [ `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`, - `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, docker: { @@ -275,22 +254,10 @@ PersistentKeepalive = 5`; environment: - PANGOLIN_ENDPOINT=${endpoint} - NEWT_ID=${id} - - NEWT_SECRET=${secret}${acceptClientsEnv}` + - NEWT_SECRET=${secret}` ], "Docker Run": [ - `docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ] - }, - kubernetes: { - "Helm Chart": [ - `helm repo add fossorial https://charts.fossorial.io`, - `helm repo update fossorial`, - `helm install newt fossorial/newt \\ - --create-namespace \\ - --set newtInstances[0].name="main-tunnel" \\ - --set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\ - --set-string newtInstances[0].auth.keys.idKey="${id}" \\ - --set-string newtInstances[0].auth.keys.secretKey="${secret}"` + `docker run -it fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, podman: { @@ -303,7 +270,7 @@ ContainerName=newt Image=docker.io/fosrl/newt Environment=PANGOLIN_ENDPOINT=${endpoint} Environment=NEWT_ID=${id} -Environment=NEWT_SECRET=${secret}${acceptClients ? "\nEnvironment=ACCEPT_CLIENTS=true" : ""} +Environment=NEWT_SECRET=${secret} # Secret=newt-secret,type=env,target=NEWT_SECRET [Service] @@ -313,16 +280,8 @@ Restart=always WantedBy=default.target` ], "Podman Run": [ - `podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + `podman run -it docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}` ] - }, - 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}` - // ] } }; setCommands(commands); @@ -331,25 +290,17 @@ WantedBy=default.target` const getArchitectures = () => { switch (platform) { case "linux": - // return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; - return ["All"]; + return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; case "mac": - // return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; - return ["All"]; + return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; case "windows": return ["x64"]; case "docker": return ["Docker Compose", "Docker Run"]; - case "kubernetes": - return ["Helm Chart"]; case "podman": return ["Podman Quadlet", "Podman Run"]; case "freebsd": - // return ["amd64", "arm64"]; - return ["All"]; - case "nixos": - // return ["x86_64", "aarch64"]; - return ["All"]; + return ["amd64", "arm64"]; default: return ["x64"]; } @@ -363,21 +314,17 @@ WantedBy=default.target` return "macOS"; case "docker": return "Docker"; - case "kubernetes": - return "Kubernetes"; case "podman": return "Podman"; case "freebsd": return "FreeBSD"; - case "nixos": - return "NixOS"; default: return "Linux"; } }; const getCommand = () => { - const placeholder = [t("unknownCommand")]; + const placeholder = ["Unknown command"]; if (!commands) { return placeholder; } @@ -411,45 +358,31 @@ WantedBy=default.target` return ; case "docker": return ; - case "kubernetes": - return ; case "podman": return ; case "freebsd": return ; - case "nixos": - return ; default: return ; } }; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createSiteFormSchema), - defaultValues: { - name: "", - copied: false, - method: "newt", - clientAddress: "", - acceptClients: false, - exitNodeId: undefined - } + defaultValues: { name: "", copied: false, method: "newt" } }); async function onSubmit(data: CreateSiteFormValues) { setCreateLoading(true); - let payload: CreateSiteBody = { - name: data.name, - type: data.method as "newt" | "wireguard" | "local" - }; + let payload: CreateSiteBody = { name: data.name, type: data.method }; if (data.method == "wireguard") { if (!siteDefaults || !wgConfig) { toast({ variant: "destructive", - title: t("siteErrorCreate"), - description: t("siteErrorCreateKeyPair") + title: "Error creating site", + description: "Key pair or site defaults not found" }); setCreateLoading(false); return; @@ -466,8 +399,8 @@ WantedBy=default.target` if (!siteDefaults) { toast({ variant: "destructive", - title: t("siteErrorCreate"), - description: t("siteErrorCreateDefaults") + title: "Error creating site", + description: "Site defaults not found" }); setCreateLoading(false); return; @@ -478,8 +411,7 @@ WantedBy=default.target` subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, - newtId: siteDefaults.newtId, - address: clientAddress + newtId: siteDefaults.newtId }; } @@ -490,7 +422,7 @@ WantedBy=default.target` .catch((e) => { toast({ variant: "destructive", - title: t("siteErrorCreate"), + title: "Error creating site", description: formatAxiosError(e) }); }); @@ -508,43 +440,22 @@ WantedBy=default.target` const load = async () => { setLoadingPage(true); - let currentNewtVersion = "latest"; + let newtVersion = "latest"; try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3000); - const response = await fetch( - `https://api.github.com/repos/fosrl/newt/releases/latest`, - { signal: controller.signal } + `https://api.github.com/repos/fosrl/newt/releases/latest` ); - - clearTimeout(timeoutId); - if (!response.ok) { throw new Error( - t("newtErrorFetchReleases", { - err: response.statusText - }) + `Failed to fetch release info: ${response.statusText}` ); } const data = await response.json(); const latestVersion = data.tag_name; - currentNewtVersion = latestVersion; - setNewtVersion(latestVersion); + newtVersion = latestVersion; } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - console.error(t("newtErrorFetchTimeout")); - } else { - console.error( - t("newtErrorFetchLatest", { - err: - error instanceof Error - ? error.message - : String(error) - }) - ); - } + console.error("Error fetching latest release:", error); } const generatedKeypair = generateKeypair(); @@ -558,10 +469,8 @@ WantedBy=default.target` await api .get(`/org/${orgId}/pick-site-defaults`) .catch((e) => { - // 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"); - } + // update the default value of the form to be local method + form.setValue("method", "local"); }) .then((res) => { if (res && res.status === 200) { @@ -572,19 +481,16 @@ WantedBy=default.target` const newtId = data.newtId; const newtSecret = data.newtSecret; const newtEndpoint = data.endpoint; - const clientAddress = data.clientAddress; setNewtId(newtId); setNewtSecret(newtSecret); setNewtEndpoint(newtEndpoint); - setClientAddress(clientAddress); hydrateCommands( newtId, newtSecret, env.app.dashboardUrl, - currentNewtVersion, - acceptClients + newtVersion ); hydrateWireGuardConfig( @@ -610,29 +516,12 @@ WantedBy=default.target` load(); }, []); - // Sync form acceptClients value with local state - useEffect(() => { - 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 ( <>
@@ -650,18 +539,13 @@ WantedBy=default.target` - {t("siteInfo")} + Site Information
{ - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} className="space-y-4" id="create-site-form" > @@ -671,7 +555,7 @@ WantedBy=default.target` render={({ field }) => ( - {t("name")} + Name + + This is the display + name for the site. + )} /> - {env.flags.enableClients && - form.watch("method") === - "newt" && ( - ( - - - {t( - "siteAddress" - )} - - - { - setClientAddress( - e - .target - .value - ); - field.onChange( - e - .target - .value - ); - }} - /> - - - - {t( - "siteAddressDescription" - )} - - - )} - /> - )}
- {tunnelTypes.length > 1 && ( - - - - {t("tunnelType")} - - - {t("siteTunnelDescription")} - - - - { - form.setValue("method", value); - }} - cols={3} - /> - - - )} + + + + Tunnel Type + + + Determine how you want to connect to your + site + + + + { + form.setValue("method", value); + }} + cols={3} + /> + + {form.watch("method") === "newt" && ( <> - {t("siteNewtCredentials")} + Newt Credentials - {t( - "siteNewtCredentialsDescription" - )} + This is how Newt will authenticate + with the server - {t("newtEndpoint")} + Newt Endpoint - {t("newtId")} + Newt ID - {t("newtSecretKey")} + Newt Secret Key - {t("siteCredentialsSave")} + Save Your Credentials - {t( - "siteCredentialsSaveDescription" - )} + You will only be able to see + this once. Make sure to copy it + to a secure place. - {/*
*/} - {/* */} - {/* ( */} - {/* */} - {/*
*/} - {/* { */} - {/* form.setValue( */} - {/* "copied", */} - {/* e as boolean */} - {/* ); */} - {/* }} */} - {/* /> */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/* )} */} - {/* /> */} - {/* */} - {/* */} +
+ + ( + +
+ { + form.setValue( + "copied", + e as boolean + ); + }} + /> + +
+ +
+ )} + /> + +
+ - {t("siteInstallNewt")} + Install Newt - {t("siteInstallNewtDescription")} + Get Newt running on your system

- {t("operatingSystem")} + Operating System

{platforms.map((os) => ( @@ -883,7 +727,7 @@ WantedBy=default.target` ? "squareOutlinePrimary" : "squareOutline" } - className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`} + className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""}`} onClick={() => { setPlatform(os); }} @@ -900,8 +744,8 @@ WantedBy=default.target` {["docker", "podman"].includes( platform ) - ? t("method") - : t("architecture")} + ? "Method" + : "Architecture"}

{getArchitectures().map( @@ -914,7 +758,7 @@ WantedBy=default.target` ? "squareOutlinePrimary" : "squareOutline" } - className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`} + className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""}`} onClick={() => setArchitecture( arch @@ -926,62 +770,9 @@ WantedBy=default.target` ) )}
-

- {t("siteConfiguration")} -

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

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

-
- -
-

- {t("commands")} + Commands

- {t("WgConfiguration")} + WireGuard Configuration - {t("WgConfigurationDescription")} + Use the following configuration to + connect to your network @@ -1026,14 +818,56 @@ WantedBy=default.target` - {t("siteCredentialsSave")} + Save Your Credentials - {t( - "siteCredentialsSaveDescription" - )} + You will only be able to see this + once. Make sure to copy it to a + secure place. + +
+ + ( + +
+ { + form.setValue( + "copied", + e as boolean + ); + }} + /> + +
+ +
+ )} + /> + +
)} @@ -1047,17 +881,15 @@ WantedBy=default.target` router.push(`/${orgId}/settings/sites`); }} > - {t("cancel")} + Cancel
diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index b95c7666..442328b4 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -2,10 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; -import SitesTable, { SiteRow } from "../../../../components/SitesTable"; +import SitesTable, { SiteRow } from "./SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SitesSplashCard from "../../../../components/SitesSplashCard"; -import { getTranslations } from "next-intl/server"; +import SitesSplashCard from "./SitesSplashCard"; type SitesPageProps = { params: Promise<{ orgId: string }>; @@ -24,18 +23,16 @@ export default async function SitesPage(props: SitesPageProps) { sites = res.data.data.sites; } catch (e) {} - const t = await getTranslations(); - function formatSize(mb: number, type: string): string { if (type === "local") { return "-"; // because we are not able to track the data use in a local site right now } if (mb >= 1024 * 1024) { - return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)}); + return `${(mb / (1024 * 1024)).toFixed(2)} TB`; } else if (mb >= 1024) { - return t('gigabytes', {count: (mb / 1024).toFixed(2)}); + return `${(mb / 1024).toFixed(2)} GB`; } else { - return t('megabytes', {count: mb.toFixed(2)}); + return `${mb.toFixed(2)} MB`; } } @@ -44,16 +41,11 @@ export default async function SitesPage(props: SitesPageProps) { name: site.name, id: site.siteId, nice: site.niceId.toString(), - address: site.address?.split("/")[0], mbIn: formatSize(site.megabytesIn || 0, site.type), mbOut: formatSize(site.megabytesOut || 0, site.type), orgId: params.orgId, type: site.type as any, - online: site.online, - newtVersion: site.newtVersion || undefined, - newtUpdateAvailable: site.newtUpdateAvailable || false, - exitNodeName: site.exitNodeName || undefined, - exitNodeEndpoint: site.exitNodeEndpoint || undefined, + online: site.online }; }); @@ -62,8 +54,8 @@ export default async function SitesPage(props: SitesPageProps) { {/* */} diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx deleted file mode 100644 index 7e9e579f..00000000 --- a/src/app/admin/api-keys/[apiKeyId]/layout.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import ApiKeyProvider from "@app/providers/ApiKeyProvider"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; -import { getTranslations } from "next-intl/server"; - -interface SettingsLayoutProps { - children: React.ReactNode; - params: Promise<{ apiKeyId: string }>; -} - -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; - - const t = await getTranslations(); - - const { children } = props; - - let apiKey = null; - try { - const res = await internal.get>( - `/api-key/${params.apiKeyId}`, - await authCookieHeader() - ); - apiKey = res.data.data; - } catch (e) { - console.error(e); - redirect(`/admin/api-keys`); - } - - const navItems = [ - { - title: t('apiKeysPermissionsTitle'), - href: "/admin/api-keys/{apiKeyId}/permissions" - } - ]; - - return ( - <> - - - - {children} - - - ); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/page.tsx b/src/app/admin/api-keys/[apiKeyId]/page.tsx deleted file mode 100644 index 910d1b53..00000000 --- a/src/app/admin/api-keys/[apiKeyId]/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function ApiKeysPage(props: { - params: Promise<{ apiKeyId: string }>; -}) { - const params = await props.params; - redirect(`/admin/api-keys/${params.apiKeyId}/permissions`); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx deleted file mode 100644 index e00ae425..00000000 --- a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { Button } from "@app/components/ui/button"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ListApiKeyActionsResponse } from "@server/routers/apiKeys"; -import { AxiosResponse } from "axios"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useTranslations } from "next-intl"; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const { apiKeyId } = useParams(); - - const t = useTranslations(); - - const [loadingPage, setLoadingPage] = useState(true); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - const [loadingSavePermissions, setLoadingSavePermissions] = - useState(false); - - useEffect(() => { - async function load() { - setLoadingPage(true); - - const res = await api - .get< - AxiosResponse - >(`/api-key/${apiKeyId}/actions`) - .catch((e) => { - toast({ - variant: "destructive", - title: t('apiKeysPermissionsErrorLoadingActions'), - description: formatAxiosError( - e, - t('apiKeysPermissionsErrorLoadingActions') - ) - }); - }); - - if (res && res.status === 200) { - const data = res.data.data; - for (const action of data.actions) { - setSelectedPermissions((prev) => ({ - ...prev, - [action.actionId]: true - })); - } - } - - setLoadingPage(false); - } - - load(); - }, []); - - async function savePermissions() { - setLoadingSavePermissions(true); - - const actionsRes = await api - .post(`/api-key/${apiKeyId}/actions`, { - actionIds: Object.keys(selectedPermissions).filter( - (key) => selectedPermissions[key] - ) - }) - .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); - toast({ - variant: "destructive", - title: t('apiKeysErrorSetPermission'), - description: formatAxiosError(e) - }); - }); - - if (actionsRes && actionsRes.status === 200) { - toast({ - title: t('apiKeysPermissionsUpdated'), - description: t('apiKeysPermissionsUpdatedDescription') - }); - } - - setLoadingSavePermissions(false); - } - - return ( - <> - {!loadingPage && ( - - - - - {t('apiKeysPermissionsTitle')} - - - {t('apiKeysPermissionsGeneralSettingsDescription')} - - - - - - - - - - - - )} - - ); -} diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx deleted file mode 100644 index 65f8e46a..00000000 --- a/src/app/admin/api-keys/create/page.tsx +++ /dev/null @@ -1,392 +0,0 @@ -"use client"; - -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import HeaderTitle from "@app/components/SettingsSectionTitle"; -import { z } from "zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Input } from "@app/components/ui/input"; -import { InfoIcon } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { AxiosResponse } from "axios"; -import { useRouter } from "next/navigation"; -import { - CreateOrgApiKeyBody, - CreateOrgApiKeyResponse -} from "@server/routers/apiKeys"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import CopyToClipboard from "@app/components/CopyToClipboard"; -import moment from "moment"; -import CopyTextBox from "@app/components/CopyTextBox"; -import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; -import { useTranslations } from "next-intl"; - -export default function Page() { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const router = useRouter(); - const t = useTranslations(); - - const [loadingPage, setLoadingPage] = useState(true); - const [createLoading, setCreateLoading] = useState(false); - const [apiKey, setApiKey] = useState(null); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - - const createFormSchema = z.object({ - name: z - .string() - .min(2, { - message: t('nameMin', {len: 2}) - }) - .max(255, { - message: t('nameMax', {len: 255}) - }) - }); - - type CreateFormValues = z.infer; - - const copiedFormSchema = z - .object({ - copied: z.boolean() - }) - .refine( - (data) => { - return data.copied; - }, - { - message: t('apiKeysConfirmCopy2'), - path: ["copied"] - } - ); - - type CopiedFormValues = z.infer; - - const form = useForm({ - resolver: zodResolver(createFormSchema), - defaultValues: { - name: "" - } - }); - - const copiedForm = useForm({ - resolver: zodResolver(copiedFormSchema), - defaultValues: { - copied: true - } - }); - - async function onSubmit(data: CreateFormValues) { - setCreateLoading(true); - - const payload: CreateOrgApiKeyBody = { - name: data.name - }; - - const res = await api - .put>(`/api-key`, payload) - .catch((e) => { - toast({ - variant: "destructive", - title: t('apiKeysErrorCreate'), - description: formatAxiosError(e) - }); - }); - - if (res && res.status === 201) { - const data = res.data.data; - - console.log({ - actionIds: Object.keys(selectedPermissions).filter( - (key) => selectedPermissions[key] - ) - }); - - const actionsRes = await api - .post(`/api-key/${data.apiKeyId}/actions`, { - actionIds: Object.keys(selectedPermissions).filter( - (key) => selectedPermissions[key] - ) - }) - .catch((e) => { - console.error(t('apiKeysErrorSetPermission'), e); - toast({ - variant: "destructive", - title: t('apiKeysErrorSetPermission'), - description: formatAxiosError(e) - }); - }); - - if (actionsRes) { - setApiKey(data); - } - } - - setCreateLoading(false); - } - - async function onCopiedSubmit(data: CopiedFormValues) { - if (!data.copied) { - return; - } - - router.push(`/admin/api-keys`); - } - - useEffect(() => { - const load = async () => { - setLoadingPage(false); - }; - - load(); - }, []); - - return ( - <> -
- - -
- - {!loadingPage && ( -
- - {!apiKey && ( - <> - - - - {t('apiKeysTitle')} - - - - -
- { - if (e.key === "Enter") { - e.preventDefault(); // block default enter refresh - } - }} - className="space-y-4" - id="create-site-form" - > - ( - - - {t('name')} - - - - - - - )} - /> - - -
-
-
- - - - - {t('apiKeysGeneralSettings')} - - - {t('apiKeysGeneralSettingsDescription')} - - - - - - - - )} - - {apiKey && ( - - - - {t('apiKeysList')} - - - - - - - {t('name')} - - - - - - - - {t('created')} - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - {t('apiKeysSave')} - - - {t('apiKeysSaveDescription')} - - - - {/*

*/} - {/* {t('apiKeysInfo')} */} - {/*

*/} - - - - {/*
*/} - {/* */} - {/* ( */} - {/* */} - {/*
*/} - {/* { */} - {/* copiedForm.setValue( */} - {/* "copied", */} - {/* e as boolean */} - {/* ); */} - {/* }} */} - {/* /> */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/* )} */} - {/* /> */} - {/* */} - {/* */} -
-
- )} -
- -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx deleted file mode 100644 index e518911f..00000000 --- a/src/app/admin/api-keys/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { ListRootApiKeysResponse } from "@server/routers/apiKeys"; -import ApiKeysTable, { ApiKeyRow } from "../../../components/ApiKeysTable"; -import { getTranslations } from "next-intl/server"; - -type ApiKeyPageProps = {}; - -export const dynamic = "force-dynamic"; - -export default async function ApiKeysPage(props: ApiKeyPageProps) { - let apiKeys: ListRootApiKeysResponse["apiKeys"] = []; - try { - const res = await internal.get>( - `/api-keys`, - await authCookieHeader() - ); - apiKeys = res.data.data.apiKeys; - } catch (e) {} - - const rows: ApiKeyRow[] = apiKeys.map((key) => { - return { - name: key.name, - id: key.apiKeyId, - key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`, - createdAt: key.createdAt - }; - }); - - const t = await getTranslations(); - - return ( - <> - - - - - ); -} diff --git a/src/components/AdminIdpDataTable.tsx b/src/app/admin/idp/AdminIdpDataTable.tsx similarity index 60% rename from src/components/AdminIdpDataTable.tsx rename to src/app/admin/idp/AdminIdpDataTable.tsx index 63a0b4bb..8d64ce0b 100644 --- a/src/components/AdminIdpDataTable.tsx +++ b/src/app/admin/idp/AdminIdpDataTable.tsx @@ -3,38 +3,29 @@ import { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; import { useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; - onRefresh?: () => void; - isRefreshing?: boolean; } export function IdpDataTable({ columns, - data, - onRefresh, - isRefreshing + data }: DataTableProps) { const router = useRouter(); - const t = useTranslations(); return ( { router.push("/admin/idp/create"); }} - onRefresh={onRefresh} - isRefreshing={isRefreshing} /> ); } diff --git a/src/components/private/OrgIdpTable.tsx b/src/app/admin/idp/AdminIdpTable.tsx similarity index 57% rename from src/components/private/OrgIdpTable.tsx rename to src/app/admin/idp/AdminIdpTable.tsx index 436904a0..b2415280 100644 --- a/src/components/private/OrgIdpTable.tsx +++ b/src/app/admin/idp/AdminIdpTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { IdpDataTable } from "@app/components/private/OrgIdpDataTable"; +import { IdpDataTable } from "./AdminIdpDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useState } from "react"; @@ -10,6 +10,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 { Badge } from "@app/components/ui/badge"; import { useRouter } from "next/navigation"; import { DropdownMenu, @@ -18,48 +19,87 @@ import { 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; + orgCount: number; }; type Props = { idps: IdpRow[]; - orgId: string; }; -export default function IdpTable({ idps, orgId }: Props) { +export default function IdpTable({ idps }: 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}`); + await api.delete(`/idp/${idpId}`); toast({ - title: t("success"), - description: t("idpDeletedDescription") + title: "Success", + description: "Identity provider deleted successfully" }); setIsDeleteModalOpen(false); router.refresh(); } catch (e) { toast({ - title: t("error"), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); } }; + const getTypeDisplay = (type: string) => { + switch (type) { + case "oidc": + return "OAuth2/OIDC"; + default: + return type; + } + }; const columns: ColumnDef[] = [ + { + id: "dots", + cell: ({ row }) => { + const r = row.original; + + return ( + + + + + + + + View settings + + + { + setSelectedIdp(r); + setIsDeleteModalOpen(true); + }} + > + Delete + + + + ); + } + }, { accessorKey: "idpId", header: ({ column }) => { @@ -86,7 +126,7 @@ export default function IdpTable({ idps, orgId }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("name")} + Name ); @@ -102,16 +142,15 @@ export default function IdpTable({ idps, orgId }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("type")} + Type ); }, cell: ({ row }) => { const type = row.original.type; - const variant = row.original.variant; return ( - + {getTypeDisplay(type)} ); } }, @@ -121,43 +160,9 @@ export default function IdpTable({ idps, orgId }: Props) { const siteRow = row.original; return (
- - - - - - - - {t("viewSettings")} - - - { - setSelectedIdp(siteRow); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - - - - - @@ -179,28 +184,31 @@ export default function IdpTable({ idps, orgId }: Props) { dialog={

- {t("idpQuestionRemove", { - name: selectedIdp.name - })} + Are you sure you want to permanently delete the + identity provider {selectedIdp.name}?

- {t("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. + +

+

+ To confirm, please type the name of the identity + provider below.

-

{t("idpMessageConfirm")}

} - buttonText={t("idpConfirmDelete")} + buttonText="Confirm Delete Identity Provider" onConfirm={async () => deleteIdp(selectedIdp.idpId)} string={selectedIdp.name} - title={t("idpDelete")} + title="Delete Identity Provider" /> )} - router.push(`/${orgId}/settings/idp/create`)} - /> + ); } diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 1eca54e7..eba6baea 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -41,7 +41,24 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import CopyToClipboard from "@app/components/CopyToClipboard"; -import { useTranslations } from "next-intl"; +import { Badge } from "@app/components/ui/badge"; + +const GeneralFormSchema = z.object({ + name: z.string().min(2, { message: "Name must be at least 2 characters." }), + clientId: z.string().min(1, { message: "Client ID is required." }), + clientSecret: z.string().min(1, { message: "Client Secret is required." }), + authUrl: z.string().url({ message: "Auth URL must be a valid URL." }), + tokenUrl: z.string().url({ message: "Token URL must be a valid URL." }), + identifierPath: z + .string() + .min(1, { message: "Identifier Path is required." }), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().min(1, { message: "Scopes are required." }), + autoProvision: z.boolean().default(false) +}); + +type GeneralFormValues = z.infer; export default function GeneralPage() { const { env } = useEnvContext(); @@ -52,26 +69,8 @@ export default function GeneralPage() { const [initialLoading, setInitialLoading] = useState(true); const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`; - const t = useTranslations(); - const GeneralFormSchema = z.object({ - name: z.string().min(2, { message: t('nameMin', {len: 2}) }), - clientId: z.string().min(1, { message: t('idpClientIdRequired') }), - clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), - identifierPath: z - .string() - .min(1, { message: t('idpPathRequired') }), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().min(1, { message: t('idpScopeRequired') }), - autoProvision: z.boolean().default(false) - }); - - type GeneralFormValues = z.infer; - - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: "", @@ -108,7 +107,7 @@ export default function GeneralPage() { } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); @@ -142,14 +141,14 @@ export default function GeneralPage() { if (res.status === 200) { toast({ - title: t('success'), - description: t('idpUpdatedDescription') + title: "Success", + description: "Identity provider updated successfully" }); router.refresh(); } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); @@ -168,17 +167,18 @@ export default function GeneralPage() { - {t('idpTitle')} + General Information - {t('idpSettingsDescription')} + Configure the basic information for your identity + provider - {t('redirectUrl')} + Redirect URL @@ -189,10 +189,13 @@ export default function GeneralPage() { - {t('redirectUrlAbout')} + About Redirect URL - {t('redirectUrlAboutDescription')} + This is the URL to which users will be + redirected after authentication. You need to + configure this URL in your identity provider + settings. @@ -207,12 +210,13 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - {t('name')} + Name - {t('idpDisplayName')} + A display name for this + identity provider @@ -222,7 +226,7 @@ export default function GeneralPage() {
- {t('idpAutoProvisionUsersDescription')} + When enabled, users will be + automatically created in the system upon + first login with the ability to map + users to roles and organizations. @@ -247,10 +254,11 @@ export default function GeneralPage() { - {t('idpOidcConfigure')} + OAuth2/OIDC Configuration - {t('idpOidcConfigureDescription')} + Configure the OAuth2/OIDC provider endpoints and + credentials @@ -267,13 +275,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpClientId')} + Client ID - {t('idpClientIdDescription')} + The OAuth2 client ID + from your identity + provider @@ -286,7 +296,7 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpClientSecret')} + Client Secret - {t('idpClientSecretDescription')} + The OAuth2 client secret + from your identity + provider @@ -308,13 +320,14 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpAuthUrl')} + Authorization URL - {t('idpAuthUrlDescription')} + The OAuth2 authorization + endpoint URL @@ -327,13 +340,14 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpTokenUrl')} + Token URL - {t('idpTokenUrlDescription')} + The OAuth2 token + endpoint URL @@ -348,10 +362,11 @@ export default function GeneralPage() { - {t('idpToken')} + Token Configuration - {t('idpTokenDescription')} + Configure how to extract user information from + the ID token @@ -365,17 +380,19 @@ export default function GeneralPage() { - {t('idpJmespathAbout')} + About JMESPath - {t('idpJmespathAboutDescription')} + The paths below use JMESPath + syntax to extract values from + the ID token. - {t('idpJmespathAboutDescriptionLink')}{" "} + Learn more about JMESPath{" "} @@ -387,13 +404,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathLabel')} + Identifier Path - {t('idpJmespathLabelDescription')} + The JMESPath to the user + identifier in the ID + token @@ -406,13 +425,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathEmailPathOptional')} + Email Path (Optional) - {t('idpJmespathEmailPathOptionalDescription')} + The JMESPath to the + user's email in the ID + token @@ -425,13 +446,15 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpJmespathNamePathOptional')} + Name Path (Optional) - {t('idpJmespathNamePathOptionalDescription')} + The JMESPath to the + user's name in the ID + token @@ -444,13 +467,14 @@ export default function GeneralPage() { render={({ field }) => ( - {t('idpOidcConfigureScopes')} + Scopes - {t('idpOidcConfigureScopesDescription')} + Space-separated list of + OAuth2 scopes to request @@ -471,7 +495,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - {t('saveGeneralSettings')} + Save General Settings
diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index af64e440..559c87ef 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -5,7 +5,6 @@ import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from "next-intl/server"; interface SettingsLayoutProps { children: React.ReactNode; @@ -15,7 +14,6 @@ interface SettingsLayoutProps { export default async function SettingsLayout(props: SettingsLayoutProps) { const params = await props.params; const { children } = props; - const t = await getTranslations(); let idp = null; try { @@ -30,20 +28,20 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const navItems: HorizontalTabs = [ { - title: t('general'), + title: "General", href: `/admin/idp/${params.idpId}/general` }, { - title: t('orgPolicies'), - href: `/admin/idp/${params.idpId}/policies` + title: "Organization Policies", + href: `/admin/idp/${params.idpId}/policies`, } ]; return ( <>
diff --git a/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx new file mode 100644 index 00000000..b967fc80 --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx @@ -0,0 +1,368 @@ +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { PolicyRow } from "./PolicyTable"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { cn } from "@app/lib/cn"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import type { Org } from "@server/db/schemas"; +import { AxiosResponse } from "axios"; +import { CreateIdpOrgPolicyResponse } from "@server/routers/idp"; +import { toast } from "@app/hooks/useToast"; + +type EditPolicyFormProps = { + idpId: string; + orgs: Org[]; + policies: PolicyRow[]; + policyToEdit: PolicyRow | null; + open: boolean; + setOpen: (open: boolean) => void; + afterCreate?: (policy: PolicyRow) => void; + afterEdit?: (policy: PolicyRow) => void; +}; + +const formSchema = z.object({ + orgId: z.string(), + roleMapping: z.string().optional(), + orgMapping: z.string().optional() +}); + +export default function EditPolicyForm({ + idpId, + orgs, + policies, + policyToEdit, + open, + setOpen, + afterCreate, + afterEdit +}: EditPolicyFormProps) { + const [loading, setLoading] = useState(false); + const [orgsPopoverOpen, setOrgsPopoverOpen] = useState(false); + + const api = createApiClient(useEnvContext()); + + const defaultValues = { + roleMapping: "", + orgMapping: "" + }; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + // @ts-ignore + values: policyToEdit + ? { + orgId: policyToEdit.orgId, + roleMapping: policyToEdit.roleMapping || "", + orgMapping: policyToEdit.orgMapping || "" + } + : defaultValues + }); + + async function onSubmit(values: z.infer) { + setLoading(true); + + if (policyToEdit) { + const res = await api + .post>( + `/idp/${idpId}/org/${values.orgId}`, + { + roleMapping: values.roleMapping, + orgMapping: values.orgMapping + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to create org policy", + description: formatAxiosError( + e, + "An error occurred while updating the org policy." + ) + }); + }); + + if (res && res.status === 200) { + toast({ + variant: "default", + title: "Org policy created", + description: "The org policy has been successfully updated." + }); + + setOpen(false); + + if (afterEdit) { + afterEdit({ + orgId: values.orgId, + roleMapping: values.roleMapping ?? null, + orgMapping: values.orgMapping ?? null + }); + } + } + } else { + const res = await api + .put>( + `/idp/${idpId}/org/${values.orgId}`, + { + roleMapping: values.roleMapping, + orgMapping: values.orgMapping + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to create role", + description: formatAxiosError( + e, + "An error occurred while creating the role." + ) + }); + }); + + if (res && res.status === 201) { + toast({ + variant: "default", + title: "Org policy created", + description: "The org policy has been successfully created." + }); + + setOpen(false); + + if (afterCreate) { + afterCreate({ + orgId: values.orgId, + roleMapping: values.roleMapping ?? null, + orgMapping: values.orgMapping ?? null + }); + } + } + } + + setLoading(false); + } + + return ( + { + setOpen(val); + setLoading(false); + setOrgsPopoverOpen(false); + form.reset(); + }} + > + + + + {policyToEdit ? "Edit" : "Create"} Organization Policy + + + Configure access for an organization + + + +
+ + ( + + Organization + {policyToEdit ? ( + + ) : ( + + + + + + + + + + + + No site found. + + + {orgs.map( + (org) => { + if ( + policies.find( + ( + p + ) => + p.orgId === + org.orgId + ) + ) { + return undefined; + } + return ( + { + form.setValue( + "orgId", + org.orgId + ); + setOrgsPopoverOpen( + false + ); + }} + > + + { + org.name + } + + ); + } + )} + + + + + + )} + + + )} + /> + ( + + + Role Mapping Path (Optional) + + + + + + JMESPath to extract role information + from the ID token. The result of + this expression must return the role + name(s) as defined in the + organization as a string/list of + strings. + + + + )} + /> + ( + + + Organization Mapping Path (Optional) + + + + + + JMESPath to extract organization + information from the ID token. This + expression must return thr org ID or + true for the user to be allowed to + access the organization. + + + + )} + /> + + +
+ + + + + + +
+
+ ); +} diff --git a/src/components/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx similarity index 63% rename from src/components/PolicyDataTable.tsx rename to src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx index 89c1ed19..73ca2ff8 100644 --- a/src/components/PolicyDataTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -2,12 +2,11 @@ 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; + onAdd?: () => void; } export function PolicyDataTable({ @@ -15,19 +14,15 @@ export function PolicyDataTable({ data, onAdd }: DataTableProps) { - - const t = useTranslations(); - return ( ); } diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx new file mode 100644 index 00000000..f1c8fb2a --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { PolicyDataTable } from "./PolicyDataTable"; + +export interface PolicyRow { + orgId: string; + roleMapping: string | null; + orgMapping: string | null; +} + +type PolicyTableProps = { + policies: PolicyRow[]; + onAdd: () => void; + onEdit: (row: PolicyRow) => void; + onDelete: (row: PolicyRow) => void; +}; + +export default function PolicyTable({ + policies, + onAdd, + onEdit, + onDelete +}: PolicyTableProps) { + const columns: ColumnDef[] = [ + { + id: "actions", + cell: ({ row }) => { + const policyRow = row.original; + + return ( + <> +
+ + + + + + onEdit(policyRow)} + > + Edit Policy + + onDelete(policyRow)} + > + + Delete Policy + + + + +
+ + ); + } + }, + { + id: "orgId", + accessorKey: "orgId", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "roleMapping", + header: "Role Mapping" + }, + { + accessorKey: "orgMapping", + header: "Organization Mapping" + }, + { + id: "edit", + cell: ({ row }) => ( +
+ +
+ ) + } + ]; + + return ; +} diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index 8c895b8b..7114011b 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -1,22 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { Button } from "@app/components/ui/button"; -import { Input } from "@app/components/ui/input"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; import { Form, FormControl, @@ -26,33 +12,10 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; -import PolicyTable, { PolicyRow } from "../../../../../components/PolicyTable"; -import { AxiosResponse } from "axios"; -import { ListOrgsResponse } from "@server/routers/org"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; -import { Textarea } from "@app/components/ui/textarea"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { GetIdpResponse } from "@server/routers/idp"; +import { toast } from "@app/hooks/useToast"; +import { useRouter, useParams } from "next/navigation"; import { SettingsContainer, SettingsSection, @@ -60,66 +23,51 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm + SettingsSectionFooter } from "@app/components/Settings"; -import { useTranslations } from "next-intl"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useState, useEffect } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, ExternalLink } from "lucide-react"; +import Link from "next/link"; +import { AxiosResponse } from "axios"; +import { + GetIdpResponse, + ListIdpOrgPoliciesResponse +} from "@server/routers/idp"; +import PolicyTable, { PolicyRow } from "./PolicyTable"; +import EditPolicyForm from "./EditPolicyForm"; +import { ListOrgsResponse } from "@server/routers/org"; +import type { Org } from "@server/db/schemas"; -type Organization = { - orgId: string; - name: string; -}; +const DefaultMappingsFormSchema = z.object({ + defaultRoleMapping: z.string().optional(), + defaultOrgMapping: z.string().optional() +}); + +type DefaultMappingsFormValues = z.infer; export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { idpId } = useParams(); - const t = useTranslations(); - - const [pageLoading, setPageLoading] = useState(true); - const [addPolicyLoading, setAddPolicyLoading] = useState(false); - const [editPolicyLoading, setEditPolicyLoading] = useState(false); - const [deletePolicyLoading, setDeletePolicyLoading] = useState(false); + const [loading, setLoading] = useState(false); const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] = useState(false); const [policies, setPolicies] = useState([]); - const [organizations, setOrganizations] = useState([]); - const [showAddDialog, setShowAddDialog] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); + const [editPolicyFormOpen, setEditPolicyFormOpen] = useState(false); + const [policyToEdit, setPolicyToEdit] = useState(null); + const [orgs, setOrgs] = useState([]); - const policyFormSchema = z.object({ - orgId: z.string().min(1, { message: t('orgRequired') }), - roleMapping: z.string().optional(), - orgMapping: z.string().optional() + const defaultMappingsForm = useForm({ + resolver: zodResolver(DefaultMappingsFormSchema), + defaultValues: { defaultRoleMapping: "", defaultOrgMapping: "" } }); - const defaultMappingsSchema = z.object({ - defaultRoleMapping: z.string().optional(), - defaultOrgMapping: z.string().optional() - }); - - type PolicyFormValues = z.infer; - type DefaultMappingsValues = z.infer; - - const form = useForm({ - resolver: zodResolver(policyFormSchema), - defaultValues: { - orgId: "", - roleMapping: "", - orgMapping: "" - } - }); - - const defaultMappingsForm = useForm({ - resolver: zodResolver(defaultMappingsSchema), - defaultValues: { - defaultRoleMapping: "", - defaultOrgMapping: "" - } - }); - - const loadIdp = async () => { + async function loadIdp() { try { const res = await api.get>( `/idp/${idpId}` @@ -133,160 +81,56 @@ export default function PoliciesPage() { } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); } - }; + } - const loadPolicies = async () => { + async function loadIdpOrgPolicies() { try { - const res = await api.get(`/idp/${idpId}/org`); + const res = await api.get< + AxiosResponse + >(`/idp/${idpId}/org`); if (res.status === 200) { setPolicies(res.data.data.policies); } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); } - }; + } - const loadOrganizations = async () => { + async function loadOrgs() { try { - const res = await api.get>("/orgs"); + const res = await api.get>(`/orgs`); if (res.status === 200) { - const existingOrgIds = policies.map((p) => p.orgId); - const availableOrgs = res.data.data.orgs.filter( - (org) => !existingOrgIds.includes(org.orgId) - ); - setOrganizations(availableOrgs); + setOrgs(res.data.data.orgs); } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); } - }; + } useEffect(() => { - async function load() { - setPageLoading(true); - await loadPolicies(); - await loadIdp(); - setPageLoading(false); - } + const load = async () => { + setLoading(true); + await Promise.all([loadIdp(), loadIdpOrgPolicies(), loadOrgs()]); + setLoading(false); + }; + load(); - }, [idpId]); + }, [idpId, api, router]); - const onAddPolicy = async (data: PolicyFormValues) => { - setAddPolicyLoading(true); - try { - const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - }); - if (res.status === 201) { - const newPolicy = { - orgId: data.orgId, - name: - organizations.find((org) => org.orgId === data.orgId) - ?.name || "", - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - }; - setPolicies([...policies, newPolicy]); - toast({ - title: t('success'), - description: t('orgPolicyAddedDescription') - }); - setShowAddDialog(false); - form.reset(); - } - } catch (e) { - toast({ - title: t('error'), - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setAddPolicyLoading(false); - } - }; - - const onEditPolicy = async (data: PolicyFormValues) => { - if (!editingPolicy) return; - - setEditPolicyLoading(true); - try { - const res = await api.post( - `/idp/${idpId}/org/${editingPolicy.orgId}`, - { - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - ); - if (res.status === 200) { - setPolicies( - policies.map((policy) => - policy.orgId === editingPolicy.orgId - ? { - ...policy, - roleMapping: data.roleMapping, - orgMapping: data.orgMapping - } - : policy - ) - ); - toast({ - title: t('success'), - description: t('orgPolicyUpdatedDescription') - }); - setShowAddDialog(false); - setEditingPolicy(null); - form.reset(); - } - } catch (e) { - toast({ - title: t('error'), - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setEditPolicyLoading(false); - } - }; - - const onDeletePolicy = async (orgId: string) => { - setDeletePolicyLoading(true); - try { - const res = await api.delete(`/idp/${idpId}/org/${orgId}`); - if (res.status === 200) { - setPolicies( - policies.filter((policy) => policy.orgId !== orgId) - ); - toast({ - title: t('success'), - description: t('orgPolicyDeletedDescription') - }); - } - } catch (e) { - toast({ - title: t('error'), - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setDeletePolicyLoading(false); - } - }; - - const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { + async function onDefaultMappingsSubmit(data: DefaultMappingsFormValues) { setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { @@ -295,43 +139,80 @@ export default function PoliciesPage() { }); if (res.status === 200) { toast({ - title: t('success'), - description: t('defaultMappingsUpdatedDescription') + title: "Success", + description: "Default mappings updated successfully" }); } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); } finally { setUpdateDefaultMappingsLoading(false); } - }; + } - if (pageLoading) { - return null; + // Button clicks + + function onAdd() { + setPolicyToEdit(null); + setEditPolicyFormOpen(true); + } + function onEdit(row: PolicyRow) { + setPolicyToEdit(row); + setEditPolicyFormOpen(true); + } + function onDelete(row: PolicyRow) { + api.delete(`/idp/${idpId}/org/${row.orgId}`) + .then((res) => { + if (res.status === 200) { + toast({ + title: "Success", + description: "Org policy deleted successfully" + }); + const p2 = policies.filter((p) => p.orgId !== row.orgId); + setPolicies(p2); + } + }) + .catch((e) => { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + }); + } + + function afterCreate(row: PolicyRow) { + setPolicies([...policies, row]); + } + + function afterEdit(row: PolicyRow) { + const p2 = policies.map((p) => (p.orgId === row.orgId ? row : p)); + setPolicies(p2); } return ( <> - + - {t('orgPoliciesAbout')} + About Organization Policies - {/*TODO(vlalx): Validate replacing */} - {t('orgPoliciesAboutDescription')}{" "} + Organization policies are used to configure access + control for a specific organization based on the user's + ID token. For more information, see{" "} - {t('orgPoliciesAboutDescriptionLink')} + the documentation @@ -340,20 +221,23 @@ export default function PoliciesPage() { - {t('defaultMappingsOptional')} + Default Mappings (Optional) - {t('defaultMappingsOptionalDescription')} + The default mappings are used when there is no + organization policy defined for an organization. You + can specify the default role and organization + mappings to fall back to here.
( - {t('defaultMappingsRole')} + Default Role Mapping - {t('defaultMappingsRoleDescription')} + JMESPath to extract role + information from the ID + token. The result of this + expression must return the + role name(s) as defined in + the organization as a + string/list of strings. )} /> - ( - {t('defaultMappingsOrg')} + Default Organization Mapping - {t('defaultMappingsOrgDescription')} + JMESPath to extract + organization information + from the ID token. This + expression must return thr + org ID or true for the user + to be allowed to access the + organization. @@ -402,7 +297,7 @@ export default function PoliciesPage() { form="policy-default-mappings-form" loading={updateDefaultMappingsLoading} > - {t('defaultMappingsSubmit')} + Save Default Mappings @@ -410,204 +305,22 @@ export default function PoliciesPage() { { - loadOrganizations(); - form.reset({ - orgId: "", - roleMapping: "", - orgMapping: "" - }); - setEditingPolicy(null); - setShowAddDialog(true); - }} - onEdit={(policy) => { - setEditingPolicy(policy); - form.reset({ - orgId: policy.orgId, - roleMapping: policy.roleMapping || "", - orgMapping: policy.orgMapping || "" - }); - setShowAddDialog(true); - }} + onAdd={onAdd} + onEdit={onEdit} + onDelete={onDelete} + /> + + - - { - setShowAddDialog(val); - setEditingPolicy(null); - form.reset(); - }} - > - - - - {editingPolicy - ? t('orgPoliciesEdit') - : t('orgPoliciesAdd')} - - - {t('orgPolicyConfig')} - - - - - - ( - - {t('org')} - {editingPolicy ? ( - - ) : ( - - - - - - - - - - - - {t('orgNotFound')} - - - {organizations.map( - ( - org - ) => ( - { - form.setValue( - "orgId", - org.orgId - ); - }} - > - - { - org.name - } - - ) - )} - - - - - - )} - - - )} - /> - - ( - - - {t('roleMappingPathOptional')} - - - - - - {t('defaultMappingsRoleDescription')} - - - - )} - /> - - ( - - - {t('orgMappingPathOptional')} - - - - - - {t('defaultMappingsOrgDescription')} - - - - )} - /> - - - - - - - - - - - ); } diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 27c26ed6..58e6667c 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -35,48 +35,47 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon, ExternalLink } from "lucide-react"; import { StrategySelect } from "@app/components/StrategySelect"; import { SwitchInput } from "@app/components/SwitchInput"; -import { useTranslations } from "next-intl"; +import { Badge } from "@app/components/ui/badge"; + +const createIdpFormSchema = z.object({ + name: z.string().min(2, { message: "Name must be at least 2 characters." }), + type: z.enum(["oidc"]), + clientId: z.string().min(1, { message: "Client ID is required." }), + clientSecret: z.string().min(1, { message: "Client Secret is required." }), + authUrl: z.string().url({ message: "Auth URL must be a valid URL." }), + tokenUrl: z.string().url({ message: "Token URL must be a valid URL." }), + identifierPath: z + .string() + .min(1, { message: "Identifier Path is required." }), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().min(1, { message: "Scopes are required." }), + autoProvision: z.boolean().default(false) +}); + +type CreateIdpFormValues = z.infer; + +interface ProviderTypeOption { + id: "oidc"; + title: string; + description: string; +} + +const providerTypes: ReadonlyArray = [ + { + id: "oidc", + title: "OAuth2/OIDC", + description: "Configure an OpenID Connect identity provider" + } +]; export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const [createLoading, setCreateLoading] = useState(false); - const t = useTranslations(); - const createIdpFormSchema = z.object({ - name: z.string().min(2, { message: t('nameMin', {len: 2}) }), - type: z.enum(["oidc"]), - clientId: z.string().min(1, { message: t('idpClientIdRequired') }), - clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), - authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), - tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), - identifierPath: z - .string() - .min(1, { message: t('idpPathRequired') }), - emailPath: z.string().optional(), - namePath: z.string().optional(), - scopes: z.string().min(1, { message: t('idpScopeRequired') }), - autoProvision: z.boolean().default(false) - }); - - type CreateIdpFormValues = z.infer; - - interface ProviderTypeOption { - id: "oidc"; - title: string; - description: string; - } - - const providerTypes: ReadonlyArray = [ - { - id: "oidc", - title: "OAuth2/OIDC", - description: t('idpOidcDescription') - } - ]; - - const form = useForm({ + const form = useForm({ resolver: zodResolver(createIdpFormSchema), defaultValues: { name: "", @@ -114,14 +113,14 @@ export default function Page() { if (res.status === 201) { toast({ - title: t('success'), - description: t('idpCreatedDescription') + title: "Success", + description: "Identity provider created successfully" }); router.push(`/admin/idp/${res.data.data.idpId}`); } } catch (e) { toast({ - title: t('error'), + title: "Error", description: formatAxiosError(e), variant: "destructive" }); @@ -134,8 +133,8 @@ export default function Page() { <>
@@ -151,10 +150,11 @@ export default function Page() { - {t('idpTitle')} + General Information - {t('idpCreateSettingsDescription')} + Configure the basic information for your identity + provider @@ -170,12 +170,13 @@ export default function Page() { name="name" render={({ field }) => ( - {t('name')} + Name - {t('idpDisplayName')} + A display name for this + identity provider @@ -185,7 +186,7 @@ export default function Page() {
- {t('idpAutoProvisionUsersDescription')} + When enabled, users will be + automatically created in the system upon + first login with the ability to map + users to roles and organizations. @@ -209,10 +213,11 @@ export default function Page() { - {t('idpType')} + Provider Type - {t('idpTypeDescription')} + Select the type of identity provider you want to + configure @@ -232,10 +237,11 @@ export default function Page() { - {t('idpOidcConfigure')} + OAuth2/OIDC Configuration - {t('idpOidcConfigureDescription')} + Configure the OAuth2/OIDC provider endpoints + and credentials @@ -251,13 +257,15 @@ export default function Page() { render={({ field }) => ( - {t('idpClientId')} + Client ID - {t('idpClientIdDescription')} + The OAuth2 client ID + from your identity + provider @@ -270,7 +278,7 @@ export default function Page() { render={({ field }) => ( - {t('idpClientSecret')} + Client Secret - {t('idpClientSecretDescription')} + The OAuth2 client secret + from your identity + provider @@ -292,7 +302,7 @@ export default function Page() { render={({ field }) => ( - {t('idpAuthUrl')} + Authorization URL - {t('idpAuthUrlDescription')} + The OAuth2 authorization + endpoint URL @@ -314,7 +325,7 @@ export default function Page() { render={({ field }) => ( - {t('idpTokenUrl')} + Token URL - {t('idpTokenUrlDescription')} + The OAuth2 token + endpoint URL @@ -335,10 +347,14 @@ export default function Page() { - {t('idpOidcConfigureAlert')} + Important Information - {t('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. @@ -347,10 +363,11 @@ export default function Page() { - {t('idpToken')} + Token Configuration - {t('idpTokenDescription')} + Configure how to extract user information + from the ID token @@ -363,17 +380,19 @@ export default function Page() { - {t('idpJmespathAbout')} + About JMESPath - {t('idpJmespathAboutDescription')}{" "} + The paths below use JMESPath + syntax to extract values from + the ID token. - {t('idpJmespathAboutDescriptionLink')}{" "} + Learn more about JMESPath{" "} @@ -385,13 +404,15 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathLabel')} + Identifier Path - {t('idpJmespathLabelDescription')} + The JMESPath to the user + identifier in the ID + token @@ -404,13 +425,15 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathEmailPathOptional')} + Email Path (Optional) - {t('idpJmespathEmailPathOptionalDescription')} + The JMESPath to the + user's email in the ID + token @@ -423,13 +446,15 @@ export default function Page() { render={({ field }) => ( - {t('idpJmespathNamePathOptional')} + Name Path (Optional) - {t('idpJmespathNamePathOptionalDescription')} + The JMESPath to the + user's name in the ID + token @@ -442,13 +467,14 @@ export default function Page() { render={({ field }) => ( - {t('idpOidcConfigureScopes')} + Scopes - {t('idpOidcConfigureScopesDescription')} + Space-separated list of + OAuth2 scopes to request @@ -470,7 +496,7 @@ export default function Page() { router.push("/admin/idp"); }} > - {t('cancel')} + Cancel
diff --git a/src/app/admin/idp/page.tsx b/src/app/admin/idp/page.tsx index fef0990c..54657c2d 100644 --- a/src/app/admin/idp/page.tsx +++ b/src/app/admin/idp/page.tsx @@ -2,8 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import IdpTable, { IdpRow } from "../../../components/AdminIdpTable"; -import { getTranslations } from "next-intl/server"; +import IdpTable, { IdpRow } from "./AdminIdpTable"; export default async function IdpPage() { let idps: IdpRow[] = []; @@ -17,13 +16,11 @@ export default async function IdpPage() { console.error(e); } - const t = await getTranslations(); - return ( <> diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 060f18ac..fdc6c8e7 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,6 +1,5 @@ import { Metadata } from "next"; -import { TopbarNav } from "@app/components/TopbarNav"; -import { KeyRound, Users } from "lucide-react"; +import { Users } from "lucide-react"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -10,7 +9,7 @@ import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { Layout } from "@app/components/Layout"; -import { adminNavSections } from "../navigation"; +import { adminNavItems } from "../navigation"; export const dynamic = "force-dynamic"; @@ -48,7 +47,7 @@ export default async function AdminLayout(props: LayoutProps) { return ( - + {props.children} diff --git a/src/app/admin/users/AdminUsersDataTable.tsx b/src/app/admin/users/AdminUsersDataTable.tsx new file mode 100644 index 00000000..7532a8cc --- /dev/null +++ b/src/app/admin/users/AdminUsersDataTable.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { + ColumnDef, +} from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function UsersDataTable({ + columns, + data +}: DataTableProps) { + return ( + + ); +} diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index 8e75ff24..68ad2790 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -1,9 +1,9 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { UsersDataTable } from "@app/components/AdminUsersDataTable"; +import { UsersDataTable } from "./AdminUsersDataTable"; import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { ArrowRight, ArrowUpDown } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; @@ -11,13 +11,6 @@ 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"; -import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuContent, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; export type GlobalUserRow = { id: string; @@ -28,8 +21,6 @@ export type GlobalUserRow = { idpId: number | null; idpName: string; dateCreated: string; - twoFactorEnabled: boolean | null; - twoFactorSetupRequested: boolean | null; }; type Props = { @@ -38,7 +29,6 @@ type Props = { export default function UsersTable({ users }: Props) { const router = useRouter(); - const t = useTranslations(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selected, setSelected] = useState(null); @@ -49,11 +39,11 @@ export default function UsersTable({ users }: Props) { const deleteUser = (id: string) => { api.delete(`/user/${id}`) .catch((e) => { - console.error(t("userErrorDelete"), e); + console.error("Error deleting user", e); toast({ variant: "destructive", - title: t("userErrorDelete"), - description: formatAxiosError(e, t("userErrorDelete")) + title: "Error deleting user", + description: formatAxiosError(e, "Error deleting user") }); }) .then(() => { @@ -92,7 +82,7 @@ export default function UsersTable({ users }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("username")} + Username ); @@ -108,7 +98,7 @@ export default function UsersTable({ users }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("email")} + Email ); @@ -124,7 +114,7 @@ export default function UsersTable({ users }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("name")} + Name ); @@ -140,85 +130,28 @@ export default function UsersTable({ users }: Props) { column.toggleSorting(column.getIsSorted() === "asc") } > - {t("identityProvider")} + Identity Provider ); } }, - { - accessorKey: "twoFactorEnabled", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const userRow = row.original; - - return ( -
- - {userRow.twoFactorEnabled || - userRow.twoFactorSetupRequested ? ( - - {t("enabled")} - - ) : ( - {t("disabled")} - )} - -
- ); - } - }, { id: "actions", cell: ({ row }) => { const r = row.original; return ( <> -
- - - - - - { - setSelected(r); - setIsDeleteModalOpen(true); - }} - > - {t("delete")} - - - +
@@ -239,27 +172,35 @@ export default function UsersTable({ users }: Props) { dialog={

- {t("userQuestionRemove", { - selectedUser: - selected?.email || + Are you sure you want to permanently delete{" "} + + {selected?.email || selected?.name || - selected?.username - })} + selected?.username} + {" "} + from the server?

- {t("userMessageRemove")} + + The user will be removed from all + organizations and be completely removed from + the server. +

-

{t("userMessageConfirm")}

+

+ To confirm, please type the name of the user + below. +

} - buttonText={t("userDeleteConfirm")} + buttonText="Confirm Delete User" onConfirm={async () => deleteUser(selected!.id)} string={ selected.email || selected.name || selected.username } - title={t("userDeleteServer")} + title="Delete User from Server" /> )} diff --git a/src/app/admin/users/[userId]/general/page.tsx b/src/app/admin/users/[userId]/general/page.tsx deleted file mode 100644 index ae720a6f..00000000 --- a/src/app/admin/users/[userId]/general/page.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Button } from "@app/components/ui/button"; -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"; -import { useParams } from "next/navigation"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm -} from "@app/components/Settings"; -import { UserType } from "@server/types/UserTypes"; - -export default function GeneralPage() { - const { userId } = useParams(); - const api = createApiClient(useEnvContext()); - const t = useTranslations(); - - const [loadingData, setLoadingData] = useState(true); - const [loading, setLoading] = useState(false); - const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); - const [userType, setUserType] = useState(null); - - useEffect(() => { - // Fetch current user 2FA status - const fetchUserData = async () => { - setLoadingData(true); - try { - const response = await api.get(`/user/${userId}`); - if (response.status === 200) { - const userData = response.data.data; - setTwoFactorEnabled( - userData.twoFactorEnabled || - userData.twoFactorSetupRequested - ); - setUserType(userData.type); - } - } catch (error) { - console.error("Failed to fetch user data:", error); - toast({ - variant: "destructive", - title: t("userErrorDelete"), - description: formatAxiosError(error, t("userErrorDelete")) - }); - } - setLoadingData(false); - }; - - fetchUserData(); - }, [userId]); - - const handleTwoFactorToggle = (enabled: boolean) => { - setTwoFactorEnabled(enabled); - }; - - const handleSaveSettings = async () => { - setLoading(true); - - try { - console.log("twoFactorEnabled", twoFactorEnabled); - await api.post(`/user/${userId}/2fa`, { - twoFactorSetupRequested: twoFactorEnabled - }); - - setTwoFactorEnabled(twoFactorEnabled); - } catch (error) { - toast({ - variant: "destructive", - title: t("otpErrorEnable"), - description: formatAxiosError( - error, - t("otpErrorEnableDescription") - ) - }); - } finally { - setLoading(false); - } - }; - - if (loadingData) { - return null; - } - - return ( - <> - - - - - {t("general")} - - - {t("userDescription2")} - - - - - -
- -
-
-
-
-
- -
- -
- - ); -} diff --git a/src/app/admin/users/[userId]/layout.tsx b/src/app/admin/users/[userId]/layout.tsx deleted file mode 100644 index 062b40d8..00000000 --- a/src/app/admin/users/[userId]/layout.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AdminGetUserResponse } from "@server/routers/user/adminGetUser"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; -import { cache } from "react"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from 'next-intl/server'; - -interface UserLayoutProps { - children: React.ReactNode; - params: Promise<{ userId: string }>; -} - -export default async function UserLayoutProps(props: UserLayoutProps) { - const params = await props.params; - - const { children } = props; - - const t = await getTranslations(); - - let user = null; - try { - const getUser = cache(async () => - internal.get>( - `/user/${params.userId}`, - await authCookieHeader() - ) - ); - const res = await getUser(); - user = res.data.data; - } catch { - redirect(`/admin/users`); - } - - const navItems = [ - { - title: t('general'), - href: "/admin/users/{userId}/general" - } - ]; - - return ( - <> - - - {children} - - - ); -} \ No newline at end of file diff --git a/src/app/admin/users/[userId]/page.tsx b/src/app/admin/users/[userId]/page.tsx deleted file mode 100644 index edf5aaed..00000000 --- a/src/app/admin/users/[userId]/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function UserPage(props: { - params: Promise<{ userId: string }>; -}) { - const { userId } = await props.params; - redirect(`/admin/users/${userId}/general`); -} \ No newline at end of file diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index bf6547a3..6e2290cb 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,10 +3,9 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { AdminListUsersResponse } from "@server/routers/user/adminListUsers"; -import UsersTable, { GlobalUserRow } from "../../../components/AdminUsersTable"; +import UsersTable, { GlobalUserRow } from "./AdminUsersTable"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; -import { getTranslations } from "next-intl/server"; type PageProps = { params: Promise<{ orgId: string }>; @@ -26,8 +25,6 @@ export default async function UsersPage(props: PageProps) { console.error(e); } - const t = await getTranslations(); - const userRows: GlobalUserRow[] = rows.map((row) => { return { id: row.id, @@ -36,25 +33,23 @@ export default async function UsersPage(props: PageProps) { username: row.username, type: row.type, idpId: row.idpId, - idpName: row.idpName || t('idpNameInternal'), + idpName: row.idpName || "Internal", dateCreated: row.dateCreated, - serverAdmin: row.serverAdmin, - twoFactorEnabled: row.twoFactorEnabled, - twoFactorSetupRequested: row.twoFactorSetupRequested + serverAdmin: row.serverAdmin }; }); return ( <> - {t('userAbount')} + About User Management - {t('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. diff --git a/src/app/auth/2fa/setup/page.tsx b/src/app/auth/2fa/setup/page.tsx deleted file mode 100644 index 64a6cf57..00000000 --- a/src/app/auth/2fa/setup/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@/components/ui/card"; -import TwoFactorSetupForm from "@app/components/TwoFactorSetupForm"; -import { useTranslations } from "next-intl"; -import { cleanRedirect } from "@app/lib/cleanRedirect"; - -export default function Setup2FAPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const redirect = searchParams?.get("redirect"); - const email = searchParams?.get("email"); - - const t = useTranslations(); - - // Redirect to login if no email is provided - useEffect(() => { - if (!email) { - router.push("/auth/login"); - } - }, [email, router]); - - const handleComplete = () => { - console.log("2FA setup complete", redirect, email); - if (redirect) { - const cleanUrl = cleanRedirect(redirect); - console.log("Redirecting to:", cleanUrl); - router.push(cleanUrl); - } else { - router.push("/"); - } - }; - - return ( -
- - - {t("otpSetup")} - - {t("adminEnabled2FaOnYourAccount", { email: email || "your account" })} - - - - - - -
- ); -} diff --git a/src/components/ValidateOidcToken.tsx b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx similarity index 59% rename from src/components/ValidateOidcToken.tsx rename to src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx index a99af4c7..c946869b 100644 --- a/src/components/ValidateOidcToken.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx @@ -2,6 +2,7 @@ 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"; @@ -14,8 +15,6 @@ import { } from "@/components/ui/card"; 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,7 +23,6 @@ type ValidateOidcTokenParams = { expectedState: string | undefined; stateCookie: string | undefined; idp: { name: string }; - loginPageId?: number; }; export default function ValidateOidcToken(props: ValidateOidcTokenParams) { @@ -35,61 +33,43 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const t = useTranslations(); - useEffect(() => { async function validate() { setLoading(true); - console.log(t("idpOidcTokenValidating"), { + console.log("Validating OIDC token", { code: props.code, expectedState: props.expectedState, stateCookie: props.stateCookie }); try { - const response = await validateOidcUrlCallbackProxy( - props.idpId, - props.code || "", - props.expectedState || "", - props.stateCookie || "", - props.loginPageId - ); + const res = await api.post< + AxiosResponse + >(`/auth/idp/${props.idpId}/oidc/validate-callback`, { + code: props.code, + state: props.expectedState, + storedState: props.stateCookie + }); - if (response.error) { - setError(response.message); - setLoading(false); - return; - } + console.log("Validate OIDC token response", res.data); - const data = response.data; - if (!data) { - setError("Unable to validate OIDC token"); - setLoading(false); - return; - } - - const redirectUrl = data.redirectUrl; + const redirectUrl = res.data.data.redirectUrl; if (!redirectUrl) { - router.push(env.app.dashboardUrl); + router.push("/"); } setLoading(false); await new Promise((resolve) => setTimeout(resolve, 100)); if (redirectUrl.startsWith("http")) { - window.location.href = data.redirectUrl; // this is validated by the parent using this component + window.location.href = res.data.data.redirectUrl; // this is validated by the parent using this component } else { - router.push(data.redirectUrl); + router.push(res.data.data.redirectUrl); } - } catch (e: any) { - console.error(e); - setError( - t("idpErrorOidcTokenValidating", { - defaultValue: "An unexpected error occurred. Please try again." - }) - ); + } catch (e) { + setError(formatAxiosError(e, "Error validating OIDC token")); } finally { setLoading(false); } @@ -102,24 +82,20 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
- - {t("idpConnectingTo", { name: props.idp.name })} - - - {t("idpConnectingToDescription")} - + Connecting to {props.idp.name} + Validating your identity {loading && (
- {t("idpConnectingToProcess")} + Connecting...
)} {!loading && !error && (
- {t("idpConnectingToFinished")} + Connected
)} {error && ( @@ -127,9 +103,9 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { - {t("idpErrorConnectingTo", { - name: props.idp.name - })} + There was a problem connecting to{" "} + {props.idp.name}. Please contact your + administrator. {error} diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index a2432e3e..cba74790 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -1,15 +1,8 @@ -import { cookies, headers } from "next/headers"; -import ValidateOidcToken from "@app/components/ValidateOidcToken"; -import { cache } from "react"; -import { formatAxiosError, priv } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { GetIdpResponse } from "@server/routers/idp"; -import { getTranslations } from "next-intl/server"; -import { pullEnv } from "@app/lib/pullEnv"; -import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; -import { redirect } from "next/navigation"; - -export const dynamic = "force-dynamic"; +import { cookies } from "next/headers"; +import ValidateOidcToken from "./ValidateOidcToken"; +import { idp } from "@server/db/schemas"; +import db from "@server/db"; +import { eq } from "drizzle-orm"; export default async function Page(props: { params: Promise<{ orgId: string; idpId: string }>; @@ -20,55 +13,29 @@ export default async function Page(props: { }) { const params = await props.params; const searchParams = await props.searchParams; - const t = await getTranslations(); const allCookies = await cookies(); const stateCookie = allCookies.get("p_oidc_state")?.value; + // query db directly in server component because just need the name + const [idpRes] = await db + .select({ name: idp.name }) + .from(idp) + .where(eq(idp.idpId, parseInt(params.idpId!))); - const idpRes = await cache( - async () => await priv.get>(`/idp/${params.idpId}`) - )(); - - const foundIdp = idpRes.data?.data?.idp; - - if (!foundIdp) { - return
{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); - } + if (!idpRes) { + return
IdP not found
; } return ( <> ); diff --git a/src/app/auth/initial-setup/layout.tsx b/src/app/auth/initial-setup/layout.tsx deleted file mode 100644 index 8407f0da..00000000 --- a/src/app/auth/initial-setup/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { InitialSetupCompleteResponse } from "@server/routers/auth"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; - -export default async function Layout(props: { children: React.ReactNode }) { - const setupRes = await internal.get< - AxiosResponse - >(`/auth/initial-setup-complete`, await authCookieHeader()); - const complete = setupRes.data.data.complete; - if (complete) { - redirect("/"); - } - - return
{props.children}
; -} diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 88b0f07d..9a149f75 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,8 +1,11 @@ -import ThemeSwitcher from "@app/components/ThemeSwitcher"; +import ProfileIcon from "@app/components/ProfileIcon"; +import { verifySession } from "@app/lib/auth/verifySession"; +import UserProvider from "@app/providers/UserProvider"; import { Metadata } from "next"; +import { cache } from "react"; export const metadata: Metadata = { - title: `Auth - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, + title: `Auth - Pangolin`, description: "" }; @@ -11,14 +14,23 @@ type AuthLayoutProps = { }; export default async function AuthLayout({ children }: AuthLayoutProps) { + const getUser = cache(verifySession); + const user = await getUser(); + return (
-
- -
+ {user && ( + +
+ +
+
+ )}
-
{children}
+
+ {children} +
); diff --git a/src/components/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx similarity index 58% rename from src/components/DashboardLoginForm.tsx rename to src/app/auth/login/DashboardLoginForm.tsx index 054ff664..b15dd518 100644 --- a/src/components/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -14,8 +14,6 @@ import { useRouter } from "next/navigation"; import { useEffect } from "react"; import Image from "next/image"; import { cleanRedirect } from "@app/lib/cleanRedirect"; -import BrandingLogo from "@app/components/BrandingLogo"; -import { useTranslations } from "next-intl"; type DashboardLoginFormProps = { redirect?: string; @@ -27,30 +25,40 @@ export default function DashboardLoginForm({ idps }: DashboardLoginFormProps) { const router = useRouter(); - const { env } = useEnvContext(); - const t = useTranslations(); - - function getSubtitle() { - return t("loginStart"); - } - - const logoWidth = env.branding.logo?.authPage?.width || 175; - const logoHeight = env.branding.logo?.authPage?.height || 58; + // const api = createApiClient(useEnvContext()); + // + // useEffect(() => { + // const logout = async () => { + // try { + // await api.post("/auth/logout"); + // console.log("user logged out"); + // } catch (e) {} + // }; + // + // logout(); + // }); return ( - - + +
-
-
-

{getSubtitle()}

+
+

+ Welcome to Pangolin +

+

+ Log in to get started +

- + await priv.get>("/idp") - )(); - loginIdps = idpsRes.data.data.idps.map((idp) => ({ + const idps = await db.select().from(idp); + const loginIdps = idps.map((idp) => ({ idpId: idp.idpId, - name: idp.name, - variant: idp.type + name: idp.name })) as LoginFormIDP[]; - const t = await getTranslations(); - return ( <> {isInvite && ( @@ -56,10 +47,11 @@ export default async function Page(props: {

- {t("inviteAlready")} + Looks like you've been invited!

- {t("inviteAlreadyDescription")} + To accept the invite, you must log in or create an + account.

@@ -69,7 +61,7 @@ export default async function Page(props: { {(!signUpDisabled || isInvite) && (

- {t("authNoAccount")}{" "} + Don't have an account?{" "} - {t("signup")} + Sign up

)} diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 14199493..7ddac325 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -35,7 +35,7 @@ import { ResetPasswordResponse } from "@server/routers/auth"; import { Loader2 } from "lucide-react"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Alert, AlertDescription } from "../../../components/ui/alert"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api"; @@ -44,31 +44,43 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { passwordSchema } from "@server/auth/passwordSchema"; import { cleanRedirect } from "@app/lib/cleanRedirect"; -import { useTranslations } from "next-intl"; const requestSchema = z.object({ email: z.string().email() }); +const formSchema = z + .object({ + email: z.string().email({ message: "Invalid email address" }), + token: z.string().min(8, { message: "Invalid token" }), + password: passwordSchema, + confirmPassword: passwordSchema + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords do not match" + }); + +const mfaSchema = z.object({ + code: z.string().length(6, { message: "Invalid code" }) +}); + export type ResetPasswordFormProps = { emailParam?: string; tokenParam?: string; redirect?: string; - quickstart?: boolean; }; export default function ResetPasswordForm({ emailParam, tokenParam, - redirect, - quickstart + redirect }: ResetPasswordFormProps) { const router = useRouter(); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - const t = useTranslations(); function getState() { if (emailParam && !tokenParam) { @@ -86,23 +98,7 @@ export default function ResetPasswordForm({ const api = createApiClient(useEnvContext()); - const formSchema = z - .object({ - email: z.string().email({ message: t('emailInvalid') }), - token: z.string().min(8, { message: t('tokenInvalid') }), - password: passwordSchema, - confirmPassword: passwordSchema - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: t('passwordNotMatch') - }); - - const mfaSchema = z.object({ - code: z.string().length(6, { message: t('pincodeInvalid') }) - }); - - const form = useForm({ + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: emailParam || "", @@ -112,14 +108,14 @@ export default function ResetPasswordForm({ } }); - const mfaForm = useForm({ + const mfaForm = useForm>({ resolver: zodResolver(mfaSchema), defaultValues: { code: "" } }); - const requestForm = useForm({ + const requestForm = useForm>({ resolver: zodResolver(requestSchema), defaultValues: { email: emailParam || "" @@ -139,8 +135,8 @@ export default function ResetPasswordForm({ } as RequestPasswordResetBody ) .catch((e) => { - setError(formatAxiosError(e, t('errorOccurred'))); - console.error(t('passwordErrorRequestReset'), e); + setError(formatAxiosError(e, "An error occurred")); + console.error("Failed to request reset:", e); setIsSubmitting(false); }); @@ -169,8 +165,8 @@ export default function ResetPasswordForm({ } as ResetPasswordBody ) .catch((e) => { - setError(formatAxiosError(e, t('errorOccurred'))); - console.error(t('passwordErrorReset'), e); + setError(formatAxiosError(e, "An error occurred")); + console.error("Failed to reset password:", e); setIsSubmitting(false); }); @@ -186,63 +182,17 @@ export default function ResetPasswordForm({ return; } - setSuccessMessage(quickstart ? t('accountSetupSuccess') : t('passwordResetSuccess')); + setSuccessMessage("Password reset successfully! Back to log in..."); - // Auto-login after successful password reset - try { - const loginRes = await api.post("/auth/login", { - email: form.getValues("email"), - password: form.getValues("password") - }); - - if (loginRes.data.data?.codeRequested) { - if (redirect) { - router.push(`/auth/login?redirect=${redirect}`); - } else { - router.push("/auth/login"); - } - return; + setTimeout(() => { + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); + } else { + router.push("/login"); } - - if (loginRes.data.data?.emailVerificationRequired) { - try { - await api.post("/auth/verify-email/request"); - } catch (verificationError) { - console.error("Failed to send verification code:", verificationError); - } - - if (redirect) { - router.push(`/auth/verify-email?redirect=${redirect}`); - } else { - router.push("/auth/verify-email"); - } - return; - } - - // Login successful, redirect - setTimeout(() => { - if (redirect) { - const safe = cleanRedirect(redirect); - router.push(safe); - } else { - router.push("/"); - } - setIsSubmitting(false); - }, 1500); - - } catch (loginError) { - // Auto-login failed, but password reset was successful - console.error("Auto-login failed:", loginError); - setTimeout(() => { - if (redirect) { - const safe = cleanRedirect(redirect); - router.push(safe); - } else { - router.push("/login"); - } - setIsSubmitting(false); - }, 1500); - } + setIsSubmitting(false); + }, 1500); } } @@ -250,14 +200,9 @@ export default function ResetPasswordForm({
- - {quickstart ? t('completeAccountSetup') : t('passwordReset')} - + Reset Password - {quickstart - ? t('completeAccountSetupDescription') - : t('passwordResetDescription') - } + Follow the steps to reset your password @@ -276,16 +221,14 @@ export default function ResetPasswordForm({ name="email" render={({ field }) => ( - {t('email')} + Email - {quickstart - ? t('accountSetupSent') - : t('passwordResetSent') - } + We'll send a password reset + code to this email address. )} @@ -306,7 +249,7 @@ export default function ResetPasswordForm({ name="email" render={({ field }) => ( - {t('email')} + Email ( - {quickstart - ? t('accountSetupCode') - : t('passwordResetCode') - } + Reset Code - {quickstart - ? t('accountSetupCodeDescription') - : t('passwordResetCodeDescription') - } + Check your email for the + reset code. )} @@ -354,10 +292,7 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('passwordCreate') - : t('passwordNew') - } + New Password ( - {quickstart - ? t('passwordCreateConfirm') - : t('passwordNewConfirm') - } + Confirm New Password ( - {t('pincodeAuth')} + Authenticator Code
@@ -475,8 +407,8 @@ export default function ResetPasswordForm({ )} {state === "reset" - ? (quickstart ? t('completeSetup') : t('passwordReset')) - : t('pincodeSubmit2')} + ? "Reset Password" + : "Submit Code"} )} @@ -490,10 +422,7 @@ export default function ResetPasswordForm({ {isSubmitting && ( )} - {quickstart - ? t('accountSetupSubmit') - : t('passwordResetSubmit') - } + Request Reset )} @@ -507,7 +436,7 @@ export default function ResetPasswordForm({ mfaForm.reset(); }} > - {t('passwordBack')} + Back to Password )} @@ -521,7 +450,7 @@ export default function ResetPasswordForm({ form.reset(); }} > - {t('backToEmail')} + Back to Email )}
diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index 1245ca09..73654beb 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -1,12 +1,9 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { cache } from "react"; -import ResetPasswordForm from "@app/components/ResetPasswordForm"; +import ResetPasswordForm from "./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"; @@ -15,28 +12,14 @@ export default async function Page(props: { redirect: string | undefined; email: string | undefined; token: string | undefined; - quickstart?: string | undefined; }>; }) { const searchParams = await props.searchParams; const getUser = cache(verifySession); const user = await getUser(); - const t = await getTranslations(); if (user) { - 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("/"); - } + redirect("/"); } let redirectUrl: string | undefined = undefined; @@ -50,21 +33,18 @@ export default async function Page(props: { redirect={searchParams.redirect} tokenParam={searchParams.token} emailParam={searchParams.email} - quickstart={ - searchParams.quickstart === "true" ? true : undefined - } />

- {t("loginBack")} + Go back to log in

diff --git a/src/components/AccessToken.tsx b/src/app/auth/resource/[resourceId]/AccessToken.tsx similarity index 88% rename from src/components/AccessToken.tsx rename to src/app/auth/resource/[resourceId]/AccessToken.tsx index 969a2d4e..467ea036 100644 --- a/src/components/AccessToken.tsx +++ b/src/app/auth/resource/[resourceId]/AccessToken.tsx @@ -13,7 +13,6 @@ import { AuthWithAccessTokenResponse } from "@server/routers/resource"; import { AxiosResponse } from "axios"; import Link from "next/link"; import { useEffect, useState } from "react"; -import { useTranslations } from "next-intl"; type AccessTokenProps = { token: string; @@ -30,8 +29,6 @@ export default function AccessToken({ const { env } = useEnvContext(); const api = createApiClient({ env }); - const t = useTranslations(); - function appendRequestToken(url: string, token: string) { const fullUrl = new URL(url); fullUrl.searchParams.append( @@ -79,7 +76,7 @@ export default function AccessToken({ ); } } catch (e) { - console.error(t('accessTokenError'), e); + console.error("Error checking access token", e); } finally { setLoading(false); } @@ -102,7 +99,7 @@ export default function AccessToken({ ); } } catch (e) { - console.error(t('accessTokenError'), e); + console.error("Error checking access token", e); } finally { setLoading(false); } @@ -118,9 +115,9 @@ export default function AccessToken({ function renderTitle() { if (isValid) { - return t('accessGranted'); + return "Access Granted"; } else { - return t('accessUrlInvalid'); + return "Access URL Invalid"; } } @@ -128,16 +125,18 @@ export default function AccessToken({ if (isValid) { return (
- {t('accessGrantedDescription')} + You have been granted access to this resource. Redirecting + you...
); } else { return (
- {t('accessUrlInvalidDescription')} + This shared access URL is invalid. Please contact the + resource owner for a new URL.
diff --git a/src/components/ResourceAccessDenied.tsx b/src/app/auth/resource/[resourceId]/ResourceAccessDenied.tsx similarity index 75% rename from src/components/ResourceAccessDenied.tsx rename to src/app/auth/resource/[resourceId]/ResourceAccessDenied.tsx index 871ef36f..088782a5 100644 --- a/src/components/ResourceAccessDenied.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAccessDenied.tsx @@ -9,23 +9,21 @@ import { CardTitle, } from "@app/components/ui/card"; import Link from "next/link"; -import { useTranslations } from "next-intl"; export default function ResourceAccessDenied() { - const t = useTranslations(); - return ( - {t('accessDenied')} + Access Denied - {t('accessDeniedDescription')} + You're not allowed to access this resource. If this is a mistake, + please contact the administrator.
diff --git a/src/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx similarity index 79% rename from src/components/ResourceAuthPortal.tsx rename to src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index 8372e756..428d09c2 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -31,21 +31,18 @@ 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 ResourceAccessDenied from "@app/components/ResourceAccessDenied"; import { - resourcePasswordProxy, - resourcePincodeProxy, - resourceWhitelistProxy, - resourceAccessProxy -} from "@app/actions/server"; + AuthWithPasswordResponse, + AuthWithWhitelistResponse +} from "@server/routers/resource"; +import ResourceAccessDenied from "./ResourceAccessDenied"; 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({ pin: z @@ -84,12 +81,10 @@ type ResourceAuthPortalProps = { }; redirect: string; idps?: LoginFormIDP[]; - orgId?: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); - const t = useTranslations(); const getNumMethods = () => { let colLength = 0; @@ -134,28 +129,28 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const [activeTab, setActiveTab] = useState(getDefaultSelectedMethod()); - const pinForm = useForm({ + const pinForm = useForm>({ resolver: zodResolver(pinSchema), defaultValues: { pin: "" } }); - const passwordForm = useForm({ + const passwordForm = useForm>({ resolver: zodResolver(passwordSchema), defaultValues: { password: "" } }); - const requestOtpForm = useForm({ + const requestOtpForm = useForm>({ resolver: zodResolver(requestOtpSchema), defaultValues: { email: "" } }); - const submitOtpForm = useForm({ + const submitOtpForm = useForm>({ resolver: zodResolver(submitOtpSchema), defaultValues: { email: "", @@ -172,129 +167,100 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { return fullUrl.toString(); } - const onWhitelistSubmit = async (values: any) => { + const onWhitelistSubmit = (values: any) => { setLoadingLogin(true); - setWhitelistError(null); + api.post>( + `/auth/resource/${props.resource.id}/whitelist`, + { email: values.email, otp: values.otp } + ) + .then((res) => { + setWhitelistError(null); - try { - const response = await resourceWhitelistProxy(props.resource.id, { - email: values.email, - otp: values.otp - }); + if (res.data.data.otpSent) { + setOtpState("otp_sent"); + submitOtpForm.setValue("email", values.email); + toast({ + title: "OTP Sent", + description: "An OTP has been sent to your email" + }); + return; + } - if (response.error) { - setWhitelistError(response.message); - return; - } - - 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 + const session = res.data.data.session; + if (session) { + window.location.href = appendRequestToken( + props.redirect, + session + ); + } + }) + .catch((e) => { + console.error(e); + setWhitelistError( + formatAxiosError(e, "Failed to authenticate with email") ); - } - } catch (e: any) { - console.error(e); - setWhitelistError( - t("otpEmailErrorAuthenticate", { - defaultValue: - "An unexpected error occurred. Please try again." - }) - ); - } finally { - setLoadingLogin(false); - } + }) + .then(() => setLoadingLogin(false)); }; - const onPinSubmit = async (values: z.infer) => { + const onPinSubmit = (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 + 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, "Failed to authenticate with pincode") ); - } - } catch (e: any) { - console.error(e); - setPincodeError( - t("pincodeErrorAuthenticate", { - defaultValue: - "An unexpected error occurred. Please try again." - }) - ); - } finally { - setLoadingLogin(false); - } + }) + .then(() => setLoadingLogin(false)); }; - const onPasswordSubmit = async (values: z.infer) => { + const onPasswordSubmit = (values: z.infer) => { setLoadingLogin(true); - setPasswordError(null); - try { - const response = await resourcePasswordProxy(props.resource.id, { + api.post>( + `/auth/resource/${props.resource.id}/password`, + { password: values.password - }); - - if (response.error) { - setPasswordError(response.message); - return; } - - const session = response.data!.session; - if (session) { - window.location.href = appendRequestToken( - props.redirect, - session + ) + .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, "Failed to authenticate with password") ); - } - } catch (e: any) { - console.error(e); - setPasswordError( - t("passwordErrorAuthenticate", { - defaultValue: - "An unexpected error occurred. Please try again." - }) - ); - } finally { - setLoadingLogin(false); - } + }) + .finally(() => setLoadingLogin(false)); }; async function handleSSOAuth() { let isAllowed = false; try { - const response = await resourceAccessProxy(props.resource.id); - if (response.error) { - setAccessDenied(true); - } else { - isAllowed = true; - } + await api.get(`/resource/${props.resource.id}`); + isAllowed = true; } catch (e) { setAccessDenied(true); } @@ -305,59 +271,30 @@ 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: resourceName }) - : t("authenticationRequest", { name: resourceName }); - } - - const logoWidth = env.branding.logo?.authPage?.width || 100100; - const logoHeight = env.branding.logo?.authPage?.height || 100; - return (
{!accessDenied ? (
- {!env.branding.resourceAuthPage?.hidePoweredBy && ( -
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- )} +
+ + Powered by{" "} + + Pangolin + + +
- {env.branding?.resourceAuthPage?.showLogo && ( -
- -
- )} - {getTitle()} + Authentication Required - {getSubtitle(props.resource.name)} + {numMethods > 1 + ? `Choose your preferred method to access ${props.resource.name}` + : `You must authenticate to access ${props.resource.name}`}
@@ -387,19 +324,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { {props.methods.password && ( {" "} - {t("password")} + Password )} {props.methods.sso && ( {" "} - {t("user")} + User )} {props.methods.whitelist && ( {" "} - {t("email")} + Email )} @@ -422,9 +359,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { render={({ field }) => ( - {t( - "pincodeInput" - )} + 6-digit PIN Code
@@ -493,7 +428,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { disabled={loadingLogin} > - {t("pincodeSubmit")} + Log in with PIN @@ -519,7 +454,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { render={({ field }) => ( - {t("password")} + Password - {t("passwordSubmit")} + Log In with Password @@ -561,7 +496,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { await handleSSOAuth() } @@ -589,7 +523,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { render={({ field }) => ( - {t("email")} + Email - {t( - "otpEmailDescription" - )} + A one-time + code will be + sent to this + email. @@ -622,7 +557,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { disabled={loadingLogin} > - {t("otpEmailSend")} + Send One-time Code @@ -644,9 +579,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { render={({ field }) => ( - {t( - "otpEmail" - )} + One-Time + Password + (OTP) - {t("otpEmailSubmit")} + Submit OTP diff --git a/src/components/ResourceNotFound.tsx b/src/app/auth/resource/[resourceId]/ResourceNotFound.tsx similarity index 72% rename from src/components/ResourceNotFound.tsx rename to src/app/auth/resource/[resourceId]/ResourceNotFound.tsx index 518fe488..5b101297 100644 --- a/src/components/ResourceNotFound.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceNotFound.tsx @@ -7,24 +7,20 @@ import { CardTitle, } from "@app/components/ui/card"; import Link from "next/link"; -import { getTranslations } from "next-intl/server"; export default async function ResourceNotFound() { - - const t = await getTranslations(); - return ( - {t('resourceNotFound')} + Resource Not Found - {t('resourceNotFoundDescription')} + The resource you're trying to access does not exist.
diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx similarity index 63% rename from src/app/auth/resource/[resourceGuid]/page.tsx rename to src/app/auth/resource/[resourceId]/page.tsx index f905fde3..af31de98 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -2,27 +2,23 @@ import { GetResourceAuthInfoResponse, GetExchangeTokenResponse } from "@server/routers/resource"; -import ResourceAuthPortal from "@app/components/ResourceAuthPortal"; +import ResourceAuthPortal from "./ResourceAuthPortal"; import { internal, priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; -import ResourceNotFound from "@app/components/ResourceNotFound"; -import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; -import AccessToken from "@app/components/AccessToken"; +import ResourceNotFound from "./ResourceNotFound"; +import ResourceAccessDenied from "./ResourceAccessDenied"; +import AccessToken from "./AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; -import { ListIdpsResponse } from "@server/routers/idp"; -import AutoLoginHandler from "@app/components/AutoLoginHandler"; -import { headers } from "next/headers"; -import { GetLoginPageResponse } from "@server/routers/loginPage/types"; - -export const dynamic = "force-dynamic"; +import db from "@server/db"; +import { idp } from "@server/db/schemas"; export default async function ResourceAuthPage(props: { - params: Promise<{ resourceGuid: number }>; + params: Promise<{ resourceId: number }>; searchParams: Promise<{ redirect: string | undefined; token: string | undefined; @@ -33,13 +29,11 @@ export default async function ResourceAuthPage(props: { const env = pullEnv(); - const authHeader = await authCookieHeader(); - let authInfo: GetResourceAuthInfoResponse | undefined; try { const res = await internal.get< AxiosResponse - >(`/resource/${params.resourceGuid}/auth`, authHeader); + >(`/resource/${params.resourceId}/auth`, await authCookieHeader()); if (res && res.status === 200) { authInfo = res.data.data; @@ -50,46 +44,23 @@ 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 { const serverResourceHost = new URL(authInfo.url).host; const redirectHost = new URL(searchParams.redirect).host; - const redirectPort = new URL(searchParams.redirect).port; - const serverResourceHostWithPort = `${serverResourceHost}:${redirectPort}`; if (serverResourceHost === redirectHost) { redirectUrl = searchParams.redirect; - } else if (serverResourceHostWithPort === redirectHost) { - redirectUrl = searchParams.redirect; } } catch (e) {} } @@ -107,7 +78,7 @@ export default async function ResourceAuthPage(props: { if (user && !user.emailVerified && env.flags.emailVerificationRequired) { redirect( - `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceGuid}` + `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}` ); } @@ -124,7 +95,7 @@ export default async function ResourceAuthPage(props: { const res = await priv.post< AxiosResponse >( - `/resource/${authInfo.resourceId}/get-exchange-token`, + `/resource/${params.resourceId}/get-exchange-token`, {}, await authCookieHeader() ); @@ -153,41 +124,18 @@ export default async function ResourceAuthPage(props: {
); } - let loginIdps: LoginFormIDP[] = []; - const idpsRes = await cache( - async () => await priv.get>("/idp") - )(); - loginIdps = idpsRes.data.data.idps.map((idp) => ({ + const idps = await db.select().from(idp); + const loginIdps = idps.map((idp) => ({ idpId: idp.idpId, - name: idp.name, - variant: idp.type + name: idp.name })) as LoginFormIDP[]; - if ( - !userIsUnauthorized && - isSSOOnly && - authInfo.skipToIdpId && - authInfo.skipToIdpId !== null - ) { - const idp = loginIdps.find((idp) => idp.idpId === authInfo.skipToIdpId); - if (idp) { - return ( - - ); - } - } - return ( <> {userIsUnauthorized && isSSOOnly ? ( @@ -209,7 +157,6 @@ export default async function ResourceAuthPage(props: { }} redirect={redirectUrl} idps={loginIdps} - orgId={undefined} />
)} diff --git a/src/app/auth/initial-setup/page.tsx b/src/app/auth/signup/SignupForm.tsx similarity index 59% rename from src/app/auth/initial-setup/page.tsx rename to src/app/auth/signup/SignupForm.tsx index 4a443896..9a4129b4 100644 --- a/src/app/auth/initial-setup/page.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; @@ -10,7 +9,6 @@ import { Input } from "@/components/ui/input"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, @@ -24,37 +22,48 @@ import { CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; -import Image from "next/image"; +import { SignUpResponse } from "@server/routers/auth"; +import { useRouter } from "next/navigation"; import { passwordSchema } from "@server/auth/passwordSchema"; +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"; + +type SignupFormProps = { + redirect?: string; + inviteId?: string; + inviteToken?: string; +}; const formSchema = z .object({ - setupToken: z.string().min(1, "Setup token is required"), email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, - confirmPassword: z.string() + confirmPassword: passwordSchema }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], message: "Passwords do not match" }); -export default function InitialSetupPage() { +export default function SignupForm({ + redirect, + inviteId, + inviteToken +}: SignupFormProps) { const router = useRouter(); + const api = createApiClient(useEnvContext()); - const t = useTranslations(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [checking, setChecking] = useState(true); - const form = useForm({ + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - setupToken: "", email: "", password: "", confirmPassword: "" @@ -62,42 +71,65 @@ export default function InitialSetupPage() { }); async function onSubmit(values: z.infer) { + const { email, password } = values; + setLoading(true); - setError(null); - try { - const res = await api.put("/auth/set-server-admin", { - setupToken: values.setupToken, - email: values.email, - password: values.password + const res = await api + .put>("/auth/signup", { + email, + password, + inviteId, + inviteToken + }) + .catch((e) => { + console.error(e); + setError( + formatAxiosError(e, "An error occurred while signing up") + ); }); - if (res && res.status === 200) { - router.replace("/"); + + if (res && res.status === 200) { + setError(null); + + if (res.data?.data?.emailVerificationRequired) { + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(`/auth/verify-email?redirect=${safe}`); + } else { + router.push("/auth/verify-email"); + } return; } - } catch (e) { - setError(formatAxiosError(e, t("setupErrorCreateAdmin"))); + + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); + } else { + router.push("/"); + } } + setLoading(false); } return ( - +
{t("pangolinLogoAlt")}

- {t("initialSetupTitle")} + Welcome to Pangolin

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

+ Create an account to get started +

@@ -106,33 +138,14 @@ export default function InitialSetupPage() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-4" > - ( - - {t("setupToken")} - - - - - {t("setupTokenDescription")} - - - - )} - /> ( - {t("email")} + Email - + @@ -143,12 +156,11 @@ export default function InitialSetupPage() { name="password" render={({ field }) => ( - {t("password")} + Password @@ -160,31 +172,26 @@ export default function InitialSetupPage() { name="confirmPassword" render={({ field }) => ( - - {t("confirmPassword")} - + Confirm Password )} /> + {error && ( {error} )} - diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index b4f4fddd..7f2205b4 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,4 +1,4 @@ -import SignupForm from "@app/components/SignupForm"; +import SignupForm from "@app/app/auth/signup/SignupForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; @@ -6,20 +6,15 @@ import { Mail } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { cache } from "react"; -import { getTranslations } from "next-intl/server"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ - redirect: string | undefined; - email: string | undefined; - }>; + searchParams: Promise<{ redirect: string | undefined }>; }) { const searchParams = await props.searchParams; const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); - const t = await getTranslations(); + const user = await getUser(); const env = pullEnv(); @@ -59,10 +54,11 @@ export default async function Page(props: {

- {t("inviteAlready")} + Looks like you've been invited!

- {t("inviteAlreadyDescription")} + To accept the invite, you must log in or create an + account.

@@ -72,11 +68,10 @@ export default async function Page(props: { redirect={redirectUrl} inviteToken={inviteToken} inviteId={inviteId} - emailParam={searchParams.email} />

- {t("signupQuestion")}{" "} + Already have an account?{" "} - {t("login")} + Log in

diff --git a/src/components/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx similarity index 70% rename from src/components/VerifyEmailForm.tsx rename to src/app/auth/verify-email/VerifyEmailForm.tsx index 052ec359..7d68263e 100644 --- a/src/components/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -10,7 +10,7 @@ import { CardContent, CardDescription, CardHeader, - CardTitle + CardTitle, } from "@/components/ui/card"; import { Form, @@ -19,25 +19,31 @@ import { FormField, FormItem, FormLabel, - FormMessage + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { InputOTP, InputOTPGroup, - InputOTPSlot + InputOTPSlot, } from "@/components/ui/input-otp"; import { AxiosResponse } from "axios"; import { VerifyEmailResponse } from "@server/routers/auth"; -import { ArrowRight, IdCard, Loader2 } from "lucide-react"; -import { Alert, AlertDescription } from "./ui/alert"; +import { Loader2 } from "lucide-react"; +import { Alert, AlertDescription } from "../../../components/ui/alert"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; -import { formatAxiosError } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api";; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { cleanRedirect } from "@app/lib/cleanRedirect"; -import { useTranslations } from "next-intl"; + +const FormSchema = z.object({ + email: z.string().email({ message: "Invalid email address" }), + pin: z.string().min(8, { + message: "Your verification code must be 8 characters.", + }), +}); export type VerifyEmailFormProps = { email: string; @@ -46,10 +52,9 @@ export type VerifyEmailFormProps = { export default function VerifyEmailForm({ email, - redirect + redirect, }: VerifyEmailFormProps) { const router = useRouter(); - const t = useTranslations(); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); @@ -58,34 +63,12 @@ export default function VerifyEmailForm({ const api = createApiClient(useEnvContext()); - function logout() { - api.post("/auth/logout") - .catch((e) => { - console.error(t("logoutError"), e); - toast({ - title: t("logoutError"), - description: formatAxiosError(e, t("logoutError")) - }); - }) - .then(() => { - router.push("/auth/login"); - router.refresh(); - }); - } - - const FormSchema = z.object({ - email: z.string().email({ message: t("emailInvalid") }), - pin: z.string().min(8, { - message: t("verificationCodeLengthRequirements") - }) - }); - - const form = useForm({ + const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { email: email, - pin: "" - } + pin: "", + }, }); async function onSubmit(data: z.infer) { @@ -93,17 +76,19 @@ export default function VerifyEmailForm({ const res = await api .post>("/auth/verify-email", { - code: data.pin + code: data.pin, }) .catch((e) => { - setError(formatAxiosError(e, t("errorOccurred"))); - console.error(t("emailErrorVerify"), e); + setError(formatAxiosError(e, "An error occurred")); + console.error("Failed to verify email:", e); setIsSubmitting(false); }); if (res && res.data?.data?.valid) { setError(null); - setSuccessMessage(t("emailVerified")); + setSuccessMessage( + "Email successfully verified! Redirecting you..." + ); setTimeout(() => { if (redirect) { const safe = cleanRedirect(redirect); @@ -120,16 +105,17 @@ export default function VerifyEmailForm({ setIsResending(true); const res = await api.post("/auth/verify-email/request").catch((e) => { - setError(formatAxiosError(e, t("errorOccurred"))); - console.error(t("verificationCodeErrorResend"), e); + setError(formatAxiosError(e, "An error occurred")); + console.error("Failed to resend verification code:", e); }); if (res) { setError(null); toast({ variant: "default", - title: t("verificationCodeResend"), - description: t("verificationCodeResendDescription") + title: "Verification code resent", + description: + "We've resent a verification code to your email address. Please check your inbox.", }); } @@ -140,26 +126,40 @@ export default function VerifyEmailForm({
- {t("emailVerify")} + Verify Email - {t("emailVerifyDescription")} + Enter the verification code sent to your email address. -

- {email} -

+ ( + + Email + + + + + + )} + /> + ( + Verification Code
+ + We sent a verification code to your + email address. + )} /> -
- -
- {error && ( {error} @@ -231,26 +222,29 @@ export default function VerifyEmailForm({ type="submit" className="w-full" disabled={isSubmitting} - form="verify-email-form" > {isSubmitting && ( )} - {t("submit")} - - - + +
+ +
); } diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index c549abf0..10ad809f 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -1,4 +1,4 @@ -import VerifyEmailForm from "@app/components/VerifyEmailForm"; +import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; diff --git a/src/components/OrganizationLanding.tsx b/src/app/components/OrganizationLanding.tsx similarity index 79% rename from src/components/OrganizationLanding.tsx rename to src/app/components/OrganizationLanding.tsx index 2d235c6d..58e765e6 100644 --- a/src/components/OrganizationLanding.tsx +++ b/src/app/components/OrganizationLanding.tsx @@ -11,9 +11,6 @@ import { import { Button } from "@/components/ui/button"; import Link from "next/link"; import { ArrowRight, Plus } from "lucide-react"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; - interface Organization { id: string; name: string; @@ -30,42 +27,44 @@ export default function OrganizationLanding({ }: OrganizationLandingProps) { const [selectedOrg, setSelectedOrg] = useState(null); - const { env } = useEnvContext(); - const handleOrgClick = (orgId: string) => { setSelectedOrg(orgId); }; - const t = useTranslations(); - function getDescriptionText() { if (organizations.length === 0) { if (!disableCreateOrg) { - return t("componentsErrorNoMemberCreate"); + return "You are not currently a member of any organizations. Create an organization to get started."; } else { - return t("componentsErrorNoMember"); + return "You are not currently a member of any organizations."; } } - return t("componentsMember", { count: organizations.length }); + return `You're a member of ${organizations.length} ${ + organizations.length === 1 ? "organization" : "organizations" + }.`; } return ( - {t("welcome")} + Welcome to Pangolin {getDescriptionText()} {organizations.length === 0 ? ( - !disableCreateOrg && ( + disableCreateOrg ? ( +

+ You are not currently a member of any organizations. +

+ ) : ( ) diff --git a/src/components/SupporterMessage.tsx b/src/app/components/SupporterMessage.tsx similarity index 90% rename from src/components/SupporterMessage.tsx rename to src/app/components/SupporterMessage.tsx index 2f415e14..f21cd52c 100644 --- a/src/components/SupporterMessage.tsx +++ b/src/app/components/SupporterMessage.tsx @@ -3,12 +3,8 @@ import React from "react"; import confetti from "canvas-confetti"; import { Star } from "lucide-react"; -import { useTranslations } from 'next-intl'; export default function SupporterMessage({ tier }: { tier: string }) { - - const t = useTranslations(); - return (
- {t('componentsSupporterMessage', {tier: tier})} + Thank you for supporting Pangolin as a {tier}!
); diff --git a/src/app/favicon.ico b/src/app/favicon.ico index bcaab339..0ffb1c54 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index e643cfb6..e2a6e31a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,142 +1,120 @@ @import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&display=swap"); -@import "tw-animate-css"; -@import "tailwindcss"; +@import 'tw-animate-css'; +@import 'tailwindcss'; @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.65rem; - --background: oklch(0.99 0 0); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.6717 0.1946 41.93); - --primary-foreground: oklch(0.98 0.016 73.684); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.967 0.001 286.375); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.705 0.213 47.604); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.141 0.005 285.823); - --sidebar-primary: oklch(0.705 0.213 47.604); - --sidebar-primary-foreground: oklch(0.98 0.016 73.684); - --sidebar-accent: oklch(0.967 0.001 286.375); - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); - --sidebar-border: oklch(0.92 0.004 286.32); - --sidebar-ring: oklch(0.705 0.213 47.604); + --background: hsl(0 0% 98%); + --foreground: hsl(20 0% 10%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(20 0% 10%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(20 0% 10%); + --primary: hsl(24.6 95% 53.1%); + --primary-foreground: hsl(60 9.1% 97.8%); + --secondary: hsl(60 4.8% 95.9%); + --secondary-foreground: hsl(24 9.8% 10%); + --muted: hsl(60 4.8% 85%); + --muted-foreground: hsl(25 5.3% 44.7%); + --accent: hsl(60 4.8% 90%); + --accent-foreground: hsl(24 9.8% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(60 9.1% 97.8%); + --border: hsl(20 5.9% 90%); + --input: hsl(20 5.9% 75%); + --ring: hsl(24.6 95% 53.1%); + --radius: 0.75rem; + --chart-1: hsl(12 76% 61%); + --chart-2: hsl(173 58% 39%); + --chart-3: hsl(197 37% 24%); + --chart-4: hsl(43 74% 66%); + --chart-5: hsl(27 87% 67%); } .dark { - --background: oklch(0.20 0.006 285.885); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.6717 0.1946 41.93); - --primary-foreground: oklch(0.98 0.016 73.684); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.646 0.222 41.116); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.646 0.222 41.116); - --sidebar-primary-foreground: oklch(0.98 0.016 73.684); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.646 0.222 41.116); + --background: hsl(20 0% 8%); + --foreground: hsl(60 9.1% 97.8%); + --card: hsl(20 0% 10%); + --card-foreground: hsl(60 9.1% 97.8%); + --popover: hsl(20 0% 10%); + --popover-foreground: hsl(60 9.1% 97.8%); + --primary: hsl(20.5 90.2% 48.2%); + --primary-foreground: hsl(60 9.1% 97.8%); + --secondary: hsl(12 6.5% 15%); + --secondary-foreground: hsl(60 9.1% 97.8%); + --muted: hsl(12 6.5% 25%); + --muted-foreground: hsl(24 5.4% 63.9%); + --accent: hsl(12 2.5% 15%); + --accent-foreground: hsl(60 9.1% 97.8%); + --destructive: hsl(0 72.2% 50.6%); + --destructive-foreground: hsl(60 9.1% 97.8%); + --border: hsl(12 6.5% 15%); + --input: hsl(12 6.5% 35%); + --ring: hsl(20.5 90.2% 48.2%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); + --color-background: var(--background); + --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); - - --shadow-2xs: 0 1px 1px rgba(0, 0, 0, 0.03); - --inset-shadow-2xs: inset 0 1px 1px rgba(0, 0, 1, 0.03); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); } @layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentcolor); - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } } @layer base { - * { - @apply border-border; - } + * { + @apply border-border; + } - body { - @apply bg-background text-foreground; - } -} - -p { - word-break: keep-all; - white-space: normal; + body { + @apply bg-background text-foreground; + } } diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx new file mode 100644 index 00000000..313bee66 --- /dev/null +++ b/src/app/invite/InviteStatusCard.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { createApiClient } from "@app/lib/api"; +import { Button } from "@app/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@app/components/ui/card"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { XCircle } from "lucide-react"; +import { useRouter } from "next/navigation"; + +type InviteStatusCardProps = { + type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; + token: string; +}; + +export default function InviteStatusCard({ + type, + token, +}: InviteStatusCardProps) { + const router = useRouter(); + + const api = createApiClient(useEnvContext()); + + async function goToLogin() { + await api.post("/auth/logout", {}); + router.push(`/auth/login?redirect=/invite?token=${token}`); + } + + async function goToSignup() { + await api.post("/auth/logout", {}); + router.push(`/auth/signup?redirect=/invite?token=${token}`); + } + + function renderBody() { + if (type === "rejected") { + return ( +
+

+ We're sorry, but it looks like the invite you're trying + to access has not been accepted or is no longer valid. +

+
    +
  • The invite may have expired
  • +
  • The invite might have been revoked
  • +
  • There could be a typo in the invite link
  • +
+
+ ); + } else if (type === "wrong_user") { + return ( +
+

+ We're sorry, but it looks like the invite you're trying + to access is not for this user. +

+

+ Please make sure you're logged in as the correct user. +

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

+ We're sorry, but it looks like the invite you're trying + to access is not for a user that exists. +

+

+ Please create an account first. +

+
+ ); + } + } + + function renderFooter() { + if (type === "rejected") { + return ( + + ); + } else if (type === "wrong_user") { + return ( + + ); + } else if (type === "user_does_not_exist") { + return ; + } + } + + return ( +
+ + + {/*
+
*/} + + Invite Not Accepted + +
+ {renderBody()} + + + {renderFooter()} + +
+
+ ); +} diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 2e027f77..b105c0b1 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -1,7 +1,11 @@ +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 { getTranslations } from "next-intl/server"; +import InviteStatusCard from "./InviteStatusCard"; +import { formatAxiosError } from "@app/lib/api";; export default async function InvitePage(props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; @@ -9,21 +13,19 @@ export default async function InvitePage(props: { const params = await props.searchParams; const tokenParam = params.token as string; - const emailParam = params.email as string; if (!tokenParam) { redirect("/"); } const user = await verifySession(); - const t = await getTranslations(); const parts = tokenParam.split("-"); if (parts.length !== 2) { return ( <> -

{t("inviteInvalid")}

-

{t("inviteInvalidDescription")}

+

Invalid Invite

+

The invite link is invalid.

); } @@ -31,15 +33,53 @@ 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); + }); + + 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") { + redirect(`/auth/signup?redirect=/invite?token=${params.token}`); + } + + if (!user && type === "not_logged_in") { + redirect(`/auth/login?redirect=/invite?token=${params.token}`); + } + return ( <> - + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b0fb8d24..d99c026f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,19 @@ import type { Metadata } from "next"; import "./globals.css"; import { Inter } from "next/font/google"; +import { Toaster } from "@/components/ui/toaster"; 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 - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, - description: "", - - ...(process.env.BRANDING_FAVICON_PATH - ? { - icons: { - icon: [ - { - url: process.env.BRANDING_FAVICON_PATH as string - } - ] - } - } - : {}) + title: `Dashboard - Pangolin`, + description: "" }; export const dynamic = "force-dynamic"; +// const font = Figtree({ subsets: ["latin"] }); const font = Inter({ subsets: ["latin"] }); export default async function RootLayout({ @@ -36,46 +21,28 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const locale = await getLocale(); + const env = pullEnv(); return ( - + - - - - - {/* 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 b84955dc..821f12c4 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -7,126 +7,96 @@ import { Waypoints, Combine, Fingerprint, - Workflow, KeyRound, - TicketCheck, - User, - Globe, // Added from 'dev' branch - MonitorUp, // Added from 'dev' branch - Server, - Zap, - CreditCard + TicketCheck } from "lucide-react"; -export type SidebarNavSection = { - // Added from 'dev' branch - heading: string; - items: SidebarNavItem[]; -}; - -// Merged from 'user-management-and-resources' branch export const orgLangingNavItems: SidebarNavItem[] = [ { - title: "sidebarAccount", + title: "Overview", href: "/{orgId}", - icon: + icon: } ]; -export const orgNavSections = ( - enableClients: boolean = true -): SidebarNavSection[] => [ +export const rootNavItems: SidebarNavItem[] = [ { - heading: "General", - items: [ - { - title: "sidebarSites", - href: "/{orgId}/settings/sites", - icon: - }, - { - title: "sidebarResources", - href: "/{orgId}/settings/resources", - icon: - }, - ...(enableClients - ? [ - { - title: "sidebarClients", - href: "/{orgId}/settings/clients", - icon: , - isBeta: true - } - ] - : []), - { - title: "sidebarDomains", - href: "/{orgId}/settings/domains", - icon: - } - ] + title: "Home", + href: "/", + icon: + } +]; + +export const orgNavItems: SidebarNavItem[] = [ + { + title: "Sites", + href: "/{orgId}/settings/sites", + icon: }, { - heading: "Access Control", - items: [ + title: "Resources", + href: "/{orgId}/settings/resources", + icon: + }, + { + title: "Access Control", + href: "/{orgId}/settings/access", + icon: , + autoExpand: true, + children: [ { - title: "sidebarUsers", + title: "Users", href: "/{orgId}/settings/access/users", - icon: + children: [ + { + title: "Invitations", + href: "/{orgId}/settings/access/invitations" + } + ] }, { - title: "sidebarRoles", - href: "/{orgId}/settings/access/roles", - icon: - }, - { - title: "sidebarInvitations", - href: "/{orgId}/settings/access/invitations", - icon: - }, - { - title: "sidebarShareableLinks", - href: "/{orgId}/settings/share-links", - icon: + title: "Roles", + href: "/{orgId}/settings/access/roles" } ] }, { - heading: "Organization", - items: [ - { - title: "sidebarApiKeys", - href: "/{orgId}/settings/api-keys", - icon: - }, - { - title: "sidebarSettings", - href: "/{orgId}/settings/general", - icon: - } - ] + title: "Shareable Links", + href: "/{orgId}/settings/share-links", + icon: + }, + /* + TODO: + { + title: "API Keys", + href: "/{orgId}/settings/api-keys", + icon: , + }, + */ + { + title: "Settings", + href: "/{orgId}/settings/general", + icon: } ]; -export const adminNavSections: SidebarNavSection[] = [ +export const adminNavItems: SidebarNavItem[] = [ { - heading: "Admin", - items: [ - { - title: "sidebarAllUsers", - href: "/admin/users", - icon: - }, - { - title: "sidebarApiKeys", - href: "/admin/api-keys", - icon: - }, - { - title: "sidebarIdentityProviders", - href: "/admin/idp", - icon: - }, - ] + title: "All Users", + href: "/admin/users", + icon: + }, + /* + TODO: + { + title: "API Keys", + href: "/admin/api-keys", + icon: , + }, + */ + { + title: "Identity Providers", + href: "/admin/idp", + icon: } ]; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 60c02bee..cb831311 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,17 +1,14 @@ -import { getTranslations } from "next-intl/server"; +import Link from "next/link"; export default async function NotFound() { - - const t = await getTranslations(); - return (

404

- {t('pageNotFound')} + Page Not Found

- {t('pageNotFoundDescription')} + Oops! The page you're looking for doesn't exist.

); diff --git a/src/app/page.tsx b/src/app/page.tsx index 676889f0..6cab7cbd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,12 +6,11 @@ import { ListUserOrgsResponse } from "@server/routers/org"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; -import OrganizationLanding from "@app/components/OrganizationLanding"; +import OrganizationLanding from "./components/OrganizationLanding"; import { pullEnv } from "@app/lib/pullEnv"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { Layout } from "@app/components/Layout"; -import { InitialSetupCompleteResponse } from "@server/routers/auth"; -import { cookies } from "next/headers"; +import { rootNavItems } from "./navigation"; export const dynamic = "force-dynamic"; @@ -28,17 +27,6 @@ export default async function Page(props: { const getUser = cache(verifySession); const user = await getUser({ skipCheckVerifyEmail: true }); - 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"); - } - if (!user) { if (params.redirect) { const safe = cleanRedirect(params.redirect); @@ -75,29 +63,9 @@ export default async function Page(props: { } } - const allCookies = await cookies(); - const lastOrgCookie = allCookies.get("pangolin-last-org")?.value; - - const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie); - if (lastOrgExists) { - redirect(`/${lastOrgCookie}`); - } else { - let ownedOrg = orgs.find((org) => org.isOwner); - if (!ownedOrg) { - ownedOrg = orgs[0]; - } - if (ownedOrg) { - redirect(`/${ownedOrg.orgId}`); - } else { - if (!env.flags.disableUserCreateOrg || user.serverAdmin) { - redirect("/setup"); - } - } - } - return ( - +
; diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index d2854c0c..e254037d 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -6,13 +6,14 @@ import UserProvider from "@app/providers/UserProvider"; import { Metadata } from "next"; import { redirect } from "next/navigation"; import { cache } from "react"; +import { rootNavItems } from "../navigation"; import { ListUserOrgsResponse } from "@server/routers/org"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; export const metadata: Metadata = { - title: `Setup - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, + title: `Setup - Pangolin`, description: "" }; @@ -53,7 +54,11 @@ export default async function SetupLayout({ return ( <> - +
{children}
diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 65ffc786..5420748c 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -2,6 +2,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; import { toast } from "@app/hooks/useToast"; import { useCallback, useEffect, useState } from "react"; import { @@ -11,7 +13,8 @@ import { CardHeader, CardTitle } from "@app/components/ui/card"; -import { formatAxiosError } from "@app/lib/api"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { formatAxiosError } from "@app/lib/api";; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Separator } from "@/components/ui/separator"; @@ -29,78 +32,49 @@ import { FormMessage } from "@app/components/ui/form"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { useTranslations } from "next-intl"; +import CreateSiteForm from "../[orgId]/settings/sites/CreateSiteForm"; type Step = "org" | "site" | "resources"; +const orgSchema = z.object({ + orgName: z.string().min(1, { message: "Organization name is required" }), + orgId: z.string().min(1, { message: "Organization ID is required" }) +}); + export default function StepperForm() { const [currentStep, setCurrentStep] = useState("org"); const [orgIdTaken, setOrgIdTaken] = useState(false); - const t = useTranslations(); - const { env } = useEnvContext(); const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); const [error, setError] = useState(null); - const [orgCreated, setOrgCreated] = useState(false); - const orgSchema = z.object({ - orgName: z.string().min(1, { message: t("orgNameRequired") }), - orgId: z.string().min(1, { message: t("orgIdRequired") }), - subnet: z.string().min(1, { message: t("subnetRequired") }) - }); - - const orgForm = useForm({ + const orgForm = useForm>({ resolver: zodResolver(orgSchema), defaultValues: { orgName: "", - orgId: "", - subnet: "" + orgId: "" } }); const api = createApiClient(useEnvContext()); const router = useRouter(); - // Fetch default subnet on component mount - useEffect(() => { - fetchDefaultSubnet(); - }, []); - - const fetchDefaultSubnet = async () => { - try { - const res = await api.get(`/pick-org-defaults`); - if (res && res.data && res.data.data) { - orgForm.setValue("subnet", res.data.data.subnet); - } - } catch (e) { - console.error("Failed to fetch default subnet:", e); - toast({ - title: "Error", - description: "Failed to fetch default subnet", - variant: "destructive" - }); + const checkOrgIdAvailability = useCallback(async (value: string) => { + if (loading) { + return; } - }; - - const checkOrgIdAvailability = useCallback( - async (value: string) => { - if (loading || orgCreated) { - return; - } - try { - const res = await api.get(`/org/checkId`, { - params: { - orgId: value - } - }); - setOrgIdTaken(res.status !== 404); - } catch (error) { - setOrgIdTaken(false); - } - }, - [loading, orgCreated, api] - ); + try { + const res = await api.get(`/org/checkId`, { + params: { + orgId: value + } + }); + setOrgIdTaken(res.status !== 404); + } catch (error) { + setOrgIdTaken(false); + } + }, []); const debouncedCheckOrgIdAvailability = useCallback( debounce(checkOrgIdAvailability, 300), @@ -108,14 +82,7 @@ export default function StepperForm() { ); const generateId = (name: string) => { - // Replace any character that is not a letter, number, space, or hyphen with a hyphen - // Also collapse multiple hyphens and trim - return name - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "-") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .replace(/^-+|-+$/g, ""); + return name.toLowerCase().replace(/\s+/g, "-"); }; async function orgSubmit(values: z.infer) { @@ -128,17 +95,18 @@ export default function StepperForm() { try { const res = await api.put(`/org`, { orgId: values.orgId, - name: values.orgName, - subnet: values.subnet + name: values.orgName }); if (res && res.status === 201) { - setOrgCreated(true); + // setCurrentStep("site"); router.push(`/${values.orgId}/settings/sites/create`); } } catch (e) { console.error(e); - setError(formatAxiosError(e, t("orgErrorCreate"))); + setError( + formatAxiosError(e, "An error occurred while creating org") + ); } setLoading(false); @@ -148,8 +116,10 @@ export default function StepperForm() { <> - {t("setupNewOrg")} - {t("setupCreate")} + New Organization + + Create your organization, site, and resources +
@@ -171,7 +141,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t("setupCreateOrg")} + Create Org
@@ -191,7 +161,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t("siteCreate")} + Create Site
@@ -211,7 +181,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t("setupCreateResources")} + Create Resources
@@ -230,22 +200,17 @@ export default function StepperForm() { render={({ field }) => ( - {t("setupOrgName")} + Organization Name { - // Prevent "/" in orgName input - const sanitizedValue = - e.target.value.replace( - /\//g, - "-" - ); const orgId = generateId( - sanitizedValue + e.target + .value ); orgForm.setValue( "orgId", @@ -253,21 +218,18 @@ export default function StepperForm() { ); orgForm.setValue( "orgName", - sanitizedValue + e.target.value ); debouncedCheckOrgIdAvailability( orgId ); }} - value={field.value.replace( - /\//g, - "-" - )} /> - {t("orgDisplayName")} + This is the display name for + your organization. )} @@ -278,7 +240,7 @@ export default function StepperForm() { render={({ field }) => ( - {t("orgId")} + Organization ID - {t( - "setupIdentifierMessage" - )} + This is the unique + identifier for your + organization. This is + separate from the display + name. )} /> - {env.flags.enableClients && ( - ( - - - Subnet - - - - - - - Network subnet for this - organization. A default - value has been provided. - - - )} - /> - )} - - {orgIdTaken && !orgCreated ? ( + {orgIdTaken && ( - {t("setupErrorIdentifier")} + Organization ID is already + taken. Please choose a different + one. - ) : null} + )} {error && ( @@ -348,7 +288,7 @@ export default function StepperForm() { orgIdTaken } > - {t("setupCreateOrg")} + Create Organization
diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx deleted file mode 100644 index 2db1415e..00000000 --- a/src/components/AdminIdpTable.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { IdpDataTable } from "@app/components/AdminIdpDataTable"; -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 { Badge } from "@app/components/ui/badge"; -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 "./IdpTypeBadge"; - -export type IdpRow = { - idpId: number; - name: string; - type: string; - orgCount: number; - variant?: string; -}; - -type Props = { - idps: IdpRow[]; -}; - -export default function IdpTable({ idps }: Props) { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - 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}`); - 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")} - /> - )} - - - - ); -} diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx deleted file mode 100644 index 6bca4a74..00000000 --- a/src/components/AdminUsersTable.tsx +++ /dev/null @@ -1,293 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { UsersDataTable } from "@app/components/AdminUsersDataTable"; -import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; -import { useRouter } from "next/navigation"; -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 { useTranslations } from "next-intl"; -import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuContent, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; - -export type GlobalUserRow = { - id: string; - name: string | null; - username: string; - email: string | null; - type: string; - idpId: number | null; - idpName: string; - dateCreated: string; - twoFactorEnabled: boolean | null; - twoFactorSetupRequested: boolean | null; -}; - -type Props = { - users: GlobalUserRow[]; -}; - -export default function UsersTable({ users }: Props) { - const router = useRouter(); - const t = useTranslations(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selected, setSelected] = useState(null); - const [rows, setRows] = useState(users); - - 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) => { - console.error(t("userErrorDelete"), e); - toast({ - variant: "destructive", - title: t("userErrorDelete"), - description: formatAxiosError(e, t("userErrorDelete")) - }); - }) - .then(() => { - router.refresh(); - setIsDeleteModalOpen(false); - - const newRows = rows.filter((row) => row.id !== id); - - setRows(newRows); - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "id", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "username", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "email", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "idpName", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "twoFactorEnabled", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const userRow = row.original; - - return ( -
- - {userRow.twoFactorEnabled || - userRow.twoFactorSetupRequested ? ( - - {t("enabled")} - - ) : ( - {t("disabled")} - )} - -
- ); - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( - <> -
- - - - - - { - setSelected(r); - setIsDeleteModalOpen(true); - }} - > - {t("delete")} - - - - -
- - ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- {t("userQuestionRemove", { - selectedUser: - selected?.email || - selected?.name || - selected?.username - })} -

- -

- {t("userMessageRemove")} -

- -

{t("userMessageConfirm")}

-
- } - buttonText={t("userDeleteConfirm")} - onConfirm={async () => deleteUser(selected!.id)} - string={ - selected.email || selected.name || selected.username - } - title={t("userDeleteServer")} - /> - )} - - - - ); -} diff --git a/src/components/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx deleted file mode 100644 index 58ab9252..00000000 --- a/src/components/ApiKeysDataTable.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { - ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - getPaginationRowModel, - SortingState, - getSortedRowModel, - ColumnFiltersState, - getFilteredRowModel -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableHeader, - TableRow -} from "@/components/ui/table"; -import { Button } from "@app/components/ui/button"; -import { useState } from "react"; -import { Input } from "@app/components/ui/input"; -import { DataTablePagination } from "@app/components/DataTablePagination"; -import { Plus, Search } from "lucide-react"; -import { DataTable } from "@app/components/ui/data-table"; -import { useTranslations } from "next-intl"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - addApiKey?: () => void; - onRefresh?: () => void; - isRefreshing?: boolean; -} - -export function ApiKeysDataTable({ - addApiKey, - columns, - data, - onRefresh, - isRefreshing -}: DataTableProps) { - - const t = useTranslations(); - - return ( - - ); -} diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx deleted file mode 100644 index adc150cf..00000000 --- a/src/components/ApiKeysTable.tsx +++ /dev/null @@ -1,213 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; -import { Button } from "@app/components/ui/button"; -import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -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 moment from "moment"; -import { ApiKeysDataTable } from "@app/components/ApiKeysDataTable"; -import { useTranslations } from "next-intl"; - -export type ApiKeyRow = { - id: string; - key: string; - name: string; - createdAt: string; -}; - -type ApiKeyTableProps = { - apiKeys: ApiKeyRow[]; -}; - -export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { - const router = useRouter(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selected, setSelected] = useState(null); - const [rows, setRows] = useState(apiKeys); - - const api = createApiClient(useEnvContext()); - - 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) => { - console.error(t("apiKeysErrorDelete"), e); - toast({ - variant: "destructive", - title: t("apiKeysErrorDelete"), - description: formatAxiosError( - e, - t("apiKeysErrorDeleteMessage") - ) - }); - }) - .then(() => { - router.refresh(); - setIsDeleteModalOpen(false); - - const newRows = rows.filter((row) => row.id !== apiKeyId); - - setRows(newRows); - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "key", - header: t("key"), - cell: ({ row }) => { - const r = row.original; - return {r.key}; - } - }, - { - accessorKey: "createdAt", - header: t("createdAt"), - cell: ({ row }) => { - const r = row.original; - return {moment(r.createdAt).format("lll")} ; - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( -
- - - - - - { - setSelected(r); - }} - > - {t("viewSettings")} - - { - setSelected(r); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - - - -
- - - -
-
- ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- {t("apiKeysQuestionRemove", { - selectedApiKey: - selected?.name || selected?.id - })} -

- -

- {t("apiKeysMessageRemove")} -

- -

{t("apiKeysMessageConfirm")}

-
- } - buttonText={t("apiKeysDeleteConfirm")} - onConfirm={async () => deleteSite(selected!.id)} - string={selected.name} - title={t("apiKeysDelete")} - /> - )} - - { - router.push(`/admin/api-keys/create`); - }} - onRefresh={refreshData} - isRefreshing={isRefreshing} - /> - - ); -} diff --git a/src/components/AutoLoginHandler.tsx b/src/components/AutoLoginHandler.tsx deleted file mode 100644 index 2391ece6..00000000 --- a/src/components/AutoLoginHandler.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { redirect, useRouter } from "next/navigation"; -import { - Card, - CardHeader, - CardTitle, - CardContent, - CardDescription -} from "@app/components/ui/card"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { generateOidcUrlProxy } from "@app/actions/server"; - -type AutoLoginHandlerProps = { - resourceId: number; - skipToIdpId: number; - redirectUrl: string; - orgId?: string; -}; - -export default function AutoLoginHandler({ - resourceId, - skipToIdpId, - redirectUrl, - orgId -}: AutoLoginHandlerProps) { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const router = useRouter(); - const t = useTranslations(); - - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function initiateAutoLogin() { - setLoading(true); - - let doRedirect: string | undefined; - try { - const response = await generateOidcUrlProxy( - skipToIdpId, - redirectUrl, - orgId - ); - - 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: any) { - console.error("Failed to generate OIDC URL:", e); - setError( - t("autoLoginErrorGeneratingUrl", { - defaultValue: "An unexpected error occurred. Please try again." - }) - ); - } finally { - setLoading(false); - if (doRedirect) { - redirect(doRedirect); - } - } - } - - initiateAutoLogin(); - }, []); - - return ( -
- - - {t("autoLoginTitle")} - - {t("autoLoginDescription")} - - - - {loading && ( -
- - {t("autoLoginProcessing")} -
- )} - {!loading && !error && ( -
- - {t("autoLoginRedirecting")} -
- )} - {error && ( - - - - {t("autoLoginError")} - {error} - - - )} -
-
-
- ); -} diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx deleted file mode 100644 index 25627e88..00000000 --- a/src/components/BrandingLogo.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTheme } from "next-themes"; -import Image from "next/image"; -import { useEffect, useState } from "react"; - -type BrandingLogoProps = { - width: number; - height: number; -}; - -export default function BrandingLogo(props: BrandingLogoProps) { - const { env } = useEnvContext(); - const { theme } = useTheme(); - const [path, setPath] = useState(""); // Default logo path - - useEffect(() => { - function getPath() { - let lightOrDark = theme; - - if (theme === "system" || !theme) { - lightOrDark = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - } - - if (lightOrDark === "light") { - if (env.branding.logo?.lightPath) { - return env.branding.logo.lightPath; - } - return "/logo/word_mark_black.png"; - } - - if (env.branding.logo?.darkPath) { - return env.branding.logo.darkPath; - } - return "/logo/word_mark_white.png"; - } - - const path = getPath(); - setPath(path); - }, [theme, env]); - - return ( - path && ( - Logo - ) - ); -} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx new file mode 100644 index 00000000..25366ffa --- /dev/null +++ b/src/components/Breadcrumbs.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; +import { cn } from "@app/lib/cn"; + +interface BreadcrumbItem { + label: string; + href: string; +} + +export function Breadcrumbs() { + const pathname = usePathname(); + const segments = pathname.split("/").filter(Boolean); + + const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => { + const href = `/${segments.slice(0, index + 1).join("/")}`; + let label = decodeURIComponent(segment); + return { label, href }; + }); + + return ( + + ); +} diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx deleted file mode 100644 index ec8ecacf..00000000 --- a/src/components/ClientInfoCard.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon } from "lucide-react"; -import { useClientContext } from "@app/hooks/useClientContext"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import { useTranslations } from "next-intl"; - -type ClientInfoCardProps = {}; - -export default function SiteInfoCard({}: ClientInfoCardProps) { - const { client, updateClient } = useClientContext(); - const t = useTranslations(); - - return ( - - - {t("clientInformation")} - - - <> - - {t("status")} - - {client.online ? ( -
-
- {t("online")} -
- ) : ( -
-
- {t("offline")} -
- )} -
-
- - - {t("address")} - - {client.subnet.split("/")[0]} - - -
-
-
- ); -} diff --git a/src/components/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx deleted file mode 100644 index 619f1fad..00000000 --- a/src/components/ClientsDataTable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { - ColumnDef, -} from "@tanstack/react-table"; -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, - onRefresh, - isRefreshing -}: DataTableProps) { - return ( - - ); -} diff --git a/src/components/ClientsTable.tsx b/src/components/ClientsTable.tsx deleted file mode 100644 index 425b8395..00000000 --- a/src/components/ClientsTable.tsx +++ /dev/null @@ -1,320 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { ClientsDataTable } from "@app/components/ClientsDataTable"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; -import { Button } from "@app/components/ui/button"; -import { - ArrowRight, - ArrowUpDown, - ArrowUpRight, - Check, - MoreHorizontal, - X -} from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -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 { useTranslations } from "next-intl"; - -export type ClientRow = { - id: number; - name: string; - subnet: string; - // siteIds: string; - mbIn: string; - mbOut: string; - orgId: string; - online: boolean; -}; - -type ClientTableProps = { - clients: ClientRow[]; - orgId: string; -}; - -export default function ClientsTable({ clients, orgId }: ClientTableProps) { - const router = useRouter(); - - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedClient, setSelectedClient] = useState( - null - ); - 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}`) - .catch((e) => { - console.error("Error deleting client", e); - toast({ - variant: "destructive", - title: "Error deleting client", - description: formatAxiosError(e, "Error deleting client") - }); - }) - .then(() => { - router.refresh(); - setIsDeleteModalOpen(false); - - const newRows = rows.filter((row) => row.id !== clientId); - - setRows(newRows); - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - // { - // accessorKey: "siteName", - // header: ({ column }) => { - // return ( - // - // ); - // }, - // cell: ({ row }) => { - // const r = row.original; - // return ( - // - // - // - // ); - // } - // }, - { - accessorKey: "online", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const originalRow = row.original; - if (originalRow.online) { - return ( - -
- Connected -
- ); - } else { - return ( - -
- Disconnected -
- ); - } - } - }, - { - accessorKey: "mbIn", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "mbOut", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "subnet", - header: ({ column }) => { - return ( - - ); - } - }, - { - id: "actions", - cell: ({ row }) => { - const clientRow = row.original; - return ( -
- - - - - - - {/* */} - {/* */} - {/* View settings */} - {/* */} - {/* */} - { - setSelectedClient(clientRow); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - - - -
- ); - } - } - ]; - - return ( - <> - {selectedClient && ( - { - setIsDeleteModalOpen(val); - setSelectedClient(null); - }} - dialog={ -
-

- Are you sure you want to remove the client{" "} - - {selectedClient?.name || selectedClient?.id} - {" "} - from the site and organization? -

- -

- - Once removed, the client will no longer be - able to connect to the site.{" "} - -

- -

- To confirm, please type the name of the client - below. -

-
- } - buttonText="Confirm Delete Client" - onConfirm={async () => deleteClient(selectedClient!.id)} - string={selectedClient.name} - title="Delete Client" - /> - )} - - { - router.push(`/${orgId}/settings/clients/create`); - }} - onRefresh={refreshData} - isRefreshing={isRefreshing} - /> - - ); -} diff --git a/src/components/ConfirmDeleteDialog.tsx b/src/components/ConfirmDeleteDialog.tsx index cd053a14..a928ed60 100644 --- a/src/components/ConfirmDeleteDialog.tsx +++ b/src/components/ConfirmDeleteDialog.tsx @@ -43,7 +43,6 @@ import { useOrgContext } from "@app/hooks/useOrgContext"; import { Description } from "@radix-ui/react-toast"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; type InviteUserFormProps = { open: boolean; @@ -68,11 +67,9 @@ export default function InviteUserForm({ const api = createApiClient(useEnvContext()); - const t = useTranslations(); - const formSchema = z.object({ string: z.string().refine((val) => val === string, { - message: t("inviteErrorInvalidConfirmation") + message: "Invalid confirmation" }) }); @@ -108,9 +105,7 @@ export default function InviteUserForm({ {title} -
- {dialog} -
+
{dialog}
- + - - - - - {t("containersIn", { siteName: site.name })} - - - {t("selectContainerDescription")} - - - -
- {})} - /> -
-
- - - - - -
-
- - ); -}; - -const DockerContainersTable: FC<{ - containers: Container[]; - onContainerSelect: (container: Container, port?: number) => void; - onRefresh: () => void; -}> = ({ containers, onContainerSelect, onRefresh }) => { - const [searchInput, setSearchInput] = useState(""); - const [globalFilter, setGlobalFilter] = useState(""); - const [hideContainersWithoutPorts, setHideContainersWithoutPorts] = - useState(true); - const [hideStoppedContainers, setHideStoppedContainers] = useState(false); - const [columnVisibility, setColumnVisibility] = useState({ - labels: false - }); - - const t = useTranslations(); - - useEffect(() => { - const timer = setTimeout(() => { - setGlobalFilter(searchInput); - }, 100); - - return () => clearTimeout(timer); - }, [searchInput]); - - const getExposedPorts = useCallback((container: Container): number[] => { - const ports: number[] = []; - - container.ports?.forEach((port) => { - if (port.privatePort) { - ports.push(port.privatePort); - } - }); - - return [...new Set(ports)]; // Remove duplicates - }, []); - - const globalFilterFunction = useCallback( - (row: any, columnId: string, value: string) => { - const container = row.original as Container; - const searchValue = value.toLowerCase(); - - // Search across all relevant fields - const searchableFields = [ - container.name, - container.image, - container.state, - container.status, - getContainerHostname(container), - ...Object.keys(container.networks), - ...Object.values(container.networks) - .map((n) => n.ipAddress) - .filter(Boolean), - ...getExposedPorts(container).map((p) => p.toString()), - ...Object.entries(container.labels).flat() - ]; - - return searchableFields.some((field) => - field?.toString().toLowerCase().includes(searchValue) - ); - }, - [getExposedPorts] - ); - - const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: t("containerName"), - cell: ({ row }) => ( -
{row.original.name}
- ) - }, - { - accessorKey: "image", - header: t("containerImage"), - cell: ({ row }) => ( -
- {row.original.image} -
- ) - }, - { - accessorKey: "state", - header: t("containerState"), - cell: ({ row }) => ( - - {row.original.state} - - ) - }, - { - accessorKey: "networks", - header: t("containerNetworks"), - cell: ({ row }) => { - const networks = Object.keys(row.original.networks); - return ( -
- {networks.length > 0 - ? networks.map((n) => ( - - {n} - - )) - : "-"} -
- ); - } - }, - { - accessorKey: "hostname", - header: t("containerHostnameIp"), - enableHiding: false, - cell: ({ row }) => ( -
- {getContainerHostname(row.original)} -
- ) - }, - { - accessorKey: "labels", - header: t("containerLabels"), - cell: ({ row }) => { - const labels = row.original.labels || {}; - const labelEntries = Object.entries(labels); - - if (labelEntries.length === 0) { - return -; - } - - return ( - - - - - - -
-

- {t("containerLabelsTitle")} -

-
- {labelEntries.map(([key, value]) => ( -
-
- {key} -
-
- {value || - t( - "containerLabelEmpty" - )} -
-
- ))} -
-
-
-
-
- ); - } - }, - { - accessorKey: "ports", - header: t("containerPorts"), - enableHiding: false, - cell: ({ row }) => { - const ports = getExposedPorts(row.original); - return ( -
- {ports.slice(0, 2).map((port) => ( - - ))} - {ports.length > 2 && ( - - - - - - {ports.slice(2).map((port) => ( - - ))} - - - )} -
- ); - } - }, - { - id: "actions", - header: t("containerActions"), - cell: ({ row }) => { - const ports = getExposedPorts(row.original); - return ( - - ); - } - } - ]; - - const initialFilters = useMemo(() => { - let filtered = containers; - - // Filter by port visibility - if (hideContainersWithoutPorts) { - filtered = filtered.filter((container) => { - const ports = getExposedPorts(container); - return ports.length > 0; // Show only containers WITH ports - }); - } - - // Filter by container state - if (hideStoppedContainers) { - filtered = filtered.filter((container) => { - return container.state === "running"; - }); - } - - return filtered; - }, [ - containers, - hideContainersWithoutPorts, - hideStoppedContainers, - getExposedPorts - ]); - - const table = useReactTable({ - data: initialFilters, - columns, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - globalFilterFn: globalFilterFunction, - state: { - globalFilter, - columnVisibility - }, - onGlobalFilterChange: setGlobalFilter, - onColumnVisibilityChange: setColumnVisibility - }); - - if (initialFilters.length === 0) { - return ( -
-
-
- {(hideContainersWithoutPorts || - hideStoppedContainers) && - containers.length > 0 ? ( - <> -

{t("noContainersMatchingFilters")}

-
- {hideContainersWithoutPorts && ( - - )} - {hideStoppedContainers && ( - - )} -
- - ) : ( -

{t("noContainersFound")}

- )} -
-
-
- ); - } - - return ( -
-
-
-
- - - setSearchInput(event.target.value) - } - className="pl-8" - /> - {searchInput && - table.getFilteredRowModel().rows.length > 0 && ( -
- {t("searchResultsCount", { - count: table.getFilteredRowModel().rows - .length - })} -
- )} -
-
- - - - - - - {t("filterOptions")} - - - - {t("filterPorts")} - - - {t("filterStopped")} - - {(hideContainersWithoutPorts || - hideStoppedContainers) && ( - <> - -
- -
- - )} -
-
- - - - - - - - {t("toggleColumns")} - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility( - !!value - ) - } - > - {column.id === "hostname" - ? t("containerHostnameIp") - : column.id} - - ); - })} - - -
- -
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {searchInput && !globalFilter ? ( -
-
- {t("searching")} -
- ) : ( - t("noContainersFoundMatching", { - filter: globalFilter - }) - )} - - - )} - -
-
-
- ); -}; - -function getContainerHostname(container: Container): string { - // First, try to get IP from networks - const networks = Object.values(container.networks); - for (const network of networks) { - if (network.ipAddress) { - return network.ipAddress; - } - } - - // Fallback to container name (works in Docker networks) - return container.name; -} diff --git a/src/components/CopyTextBox.tsx b/src/components/CopyTextBox.tsx index 72a99c3f..e6009019 100644 --- a/src/components/CopyTextBox.tsx +++ b/src/components/CopyTextBox.tsx @@ -3,7 +3,6 @@ import { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Copy, Check } from "lucide-react"; -import { useTranslations } from "next-intl"; type CopyTextBoxProps = { text?: string; @@ -20,7 +19,6 @@ export default function CopyTextBox({ }: CopyTextBoxProps) { const [isCopied, setIsCopied] = useState(false); const textRef = useRef(null); - const t = useTranslations(); const copyToClipboard = async () => { if (textRef.current) { @@ -29,7 +27,7 @@ export default function CopyTextBox({ setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { - console.error(t('copyTextFailed'), err); + console.error("Failed to copy text: ", err); } } }; @@ -40,7 +38,7 @@ export default function CopyTextBox({ >
                 {isCopied ? (
                     
diff --git a/src/components/CopyToClipboard.tsx b/src/components/CopyToClipboard.tsx
index b187e6c6..2ea582c2 100644
--- a/src/components/CopyToClipboard.tsx
+++ b/src/components/CopyToClipboard.tsx
@@ -1,7 +1,6 @@
 import { Check, Copy } from "lucide-react";
 import Link from "next/link";
 import { useState } from "react";
-import { useTranslations } from "next-intl";
 
 type CopyToClipboardProps = {
     text: string;
@@ -23,8 +22,6 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
 
     const displayValue = displayText ?? text;
 
-    const t = useTranslations();
-
     return (
         
{isLink ? ( @@ -32,7 +29,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => href={text} target="_blank" rel="noopener noreferrer" - className="truncate hover:underline text-sm" + className="truncate hover:underline" style={{ maxWidth: "100%" }} // Ensures truncation works within parent title={text} // Shows full text on hover > @@ -40,7 +37,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => ) : ( ) : ( )} - {t('copyText')} + Copy text
); diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx deleted file mode 100644 index 64ca5ed8..00000000 --- a/src/components/CreateDomainForm.tsx +++ /dev/null @@ -1,605 +0,0 @@ -"use client"; - -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - FormDescription -} from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { useToast } from "@app/hooks/useToast"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useState, useMemo } 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 { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; -import { formatAxiosError } from "@app/lib/api"; -import { CreateDomainResponse } from "@server/routers/domain/createOrgDomain"; -import { StrategySelect } from "@app/components/StrategySelect"; -import { AxiosResponse } from "axios"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon, AlertTriangle, Globe } from "lucide-react"; -import CopyToClipboard from "@app/components/CopyToClipboard"; -import { - InfoSection, - InfoSectionContent, - InfoSections, - InfoSectionTitle -} from "@app/components/InfoSection"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { toASCII, toUnicode } from 'punycode'; - - -// Helper functions for Unicode domain handling -function toPunycode(domain: string): string { - try { - const parts = toASCII(domain); - return parts; - } catch (error) { - return domain.toLowerCase(); - } -} - -function fromPunycode(domain: string): string { - try { - const parts = toUnicode(domain); - return parts; - } catch (error) { - return domain; - } -} - -function isValidDomainFormat(domain: string): boolean { - const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/; - - if (!unicodeRegex.test(domain)) { - return false; - } - - const parts = domain.split('.'); - for (const part of parts) { - if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) { - return false; - } - if (part.length > 63) { - return false; - } - } - - if (domain.length > 253) { - return false; - } - - return true; -} - -const formSchema = z.object({ - baseDomain: z - .string() - .min(1, "Domain is required") - .refine((val) => isValidDomainFormat(val), "Invalid domain format") - .transform((val) => toPunycode(val)), - type: z.enum(["ns", "cname", "wildcard"]) -}); - -type FormValues = z.infer; - -type CreateDomainFormProps = { - open: boolean; - setOpen: (open: boolean) => void; - onCreated?: (domain: CreateDomainResponse) => void; -}; - -export default function CreateDomainForm({ - open, - setOpen, - onCreated -}: CreateDomainFormProps) { - const [loading, setLoading] = useState(false); - const [createdDomain, setCreatedDomain] = - useState(null); - const api = createApiClient(useEnvContext()); - const t = useTranslations(); - const { toast } = useToast(); - const { org } = useOrgContext(); - const { env } = useEnvContext(); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - baseDomain: "", - type: "wildcard" - } - }); - - function reset() { - form.reset(); - setLoading(false); - setCreatedDomain(null); - } - - async function onSubmit(values: FormValues) { - setLoading(true); - try { - const response = await api.put>( - `/org/${org.org.orgId}/domain`, - values - ); - const domainData = response.data.data; - setCreatedDomain(domainData); - toast({ - title: t("success"), - description: t("domainCreatedDescription") - }); - onCreated?.(domainData); - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setLoading(false); - } - } - - const baseDomain = form.watch("baseDomain"); - const domainInputValue = form.watch("baseDomain") || ""; - - const punycodePreview = useMemo(() => { - if (!domainInputValue) return ""; - const punycode = toPunycode(domainInputValue); - return punycode !== domainInputValue.toLowerCase() ? punycode : ""; - }, [domainInputValue]); - - let domainOptions: any = []; - /* if (build != "oss" && env.flags.usePangolinDns) { - domainOptions = [ - { - id: "ns", - title: t("selectDomainTypeNsName"), - description: t("selectDomainTypeNsDescription") - }, - { - id: "cname", - title: t("selectDomainTypeCnameName"), - description: t("selectDomainTypeCnameDescription") - } - ]; - } */ - domainOptions = [ - { - id: "wildcard", - title: t("selectDomainTypeWildcardName"), - description: t("selectDomainTypeWildcardDescription") - } - ]; - - return ( - { - setOpen(val); - reset(); - }} - > - - - {t("domainAdd")} - - {t("domainAddDescription")} - - - - {!createdDomain ? ( - - - ( - - - - - )} - /> - ( - - {t("domain")} - - - - {punycodePreview && ( - - - - {t("internationaldomaindetected")} - -
-

{t("willbestoredas")} {punycodePreview}

-
-
-
-
- )} - -
- )} - /> - - - ) : ( -
- - - - {t("createDomainAddDnsRecords")} - - - {t("createDomainAddDnsRecordsDescription")} - - - -
- {createdDomain.nsRecords && - createdDomain.nsRecords.length > 0 && ( -
-

- {t("createDomainNsRecords")} -

- - - - {t("createDomainRecord")} - - -
-
- - {t( - "createDomainType" - )} - - - NS - -
-
- - {t( - "createDomainName" - )} - -
- - {fromPunycode(baseDomain)} - - {fromPunycode(baseDomain) !== baseDomain && ( - - ({baseDomain}) - - )} -
-
- - {t( - "createDomainValue" - )} - - {createdDomain.nsRecords.map( - ( - nsRecord, - index - ) => ( -
- -
- ) - )} -
-
-
-
-
- )} - - {createdDomain.cnameRecords && - createdDomain.cnameRecords.length > 0 && ( -
-

- {t("createDomainCnameRecords")} -

- - {createdDomain.cnameRecords.map( - (cnameRecord, index) => ( - - - {t( - "createDomainRecordNumber", - { - number: - index + - 1 - } - )} - - -
-
- - {t( - "createDomainType" - )} - - - CNAME - -
-
- - {t( - "createDomainName" - )} - -
- - {fromPunycode(cnameRecord.baseDomain)} - - {fromPunycode(cnameRecord.baseDomain) !== cnameRecord.baseDomain && ( - - ({cnameRecord.baseDomain}) - - )} -
-
-
- - {t( - "createDomainValue" - )} - - -
-
-
-
- ) - )} -
-
- )} - - {createdDomain.aRecords && - createdDomain.aRecords.length > 0 && ( -
-

- {t("createDomainARecords")} -

- - {createdDomain.aRecords.map( - (aRecord, index) => ( - - - {t( - "createDomainRecordNumber", - { - number: - index + - 1 - } - )} - - -
-
- - {t( - "createDomainType" - )} - - - A - -
-
- - {t( - "createDomainName" - )} - -
- - {fromPunycode(aRecord.baseDomain)} - - {fromPunycode(aRecord.baseDomain) !== aRecord.baseDomain && ( - - ({aRecord.baseDomain}) - - )} -
-
-
- - {t( - "createDomainValue" - )} - - - { - aRecord.value - } - -
-
-
-
- ) - )} -
-
- )} - {createdDomain.txtRecords && - createdDomain.txtRecords.length > 0 && ( -
-

- {t("createDomainTxtRecords")} -

- - {createdDomain.txtRecords.map( - (txtRecord, index) => ( - - - {t( - "createDomainRecordNumber", - { - number: - index + - 1 - } - )} - - -
-
- - {t( - "createDomainType" - )} - - - TXT - -
-
- - {t( - "createDomainName" - )} - -
- - {fromPunycode(txtRecord.baseDomain)} - - {fromPunycode(txtRecord.baseDomain) !== txtRecord.baseDomain && ( - - ({txtRecord.baseDomain}) - - )} -
-
-
- - {t( - "createDomainValue" - )} - - -
-
-
-
- ) - )} -
-
- )} -
- - {/*build != "oss" && env.flags.usePangolinDns && ( - - - - {t("createDomainSaveTheseRecords")} - - - {t( - "createDomainSaveTheseRecordsDescription" - )} - - - )*/} - - - - - {t("createDomainDnsPropagation")} - - - {t("createDomainDnsPropagationDescription")} - - -
- )} -
- - - - - {!createdDomain && ( - - )} - -
-
- ); -} diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx deleted file mode 100644 index 63dfc11d..00000000 --- a/src/components/CreateInternalResourceDialog.tsx +++ /dev/null @@ -1,422 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button } from "@app/components/ui/button"; -import { Input } from "@app/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { Check, ChevronsUpDown } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { toast } from "@app/hooks/useToast"; -import { useTranslations } from "next-intl"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { ListSitesResponse } from "@server/routers/site"; -import { cn } from "@app/lib/cn"; - -type Site = ListSitesResponse["sites"][0]; - -type CreateInternalResourceDialogProps = { - open: boolean; - setOpen: (val: boolean) => void; - orgId: string; - sites: Site[]; - onSuccess?: () => void; -}; - -export default function CreateInternalResourceDialog({ - open, - setOpen, - orgId, - sites, - onSuccess -}: CreateInternalResourceDialogProps) { - const t = useTranslations(); - const api = createApiClient(useEnvContext()); - const [isSubmitting, setIsSubmitting] = useState(false); - - const formSchema = z.object({ - name: z - .string() - .min(1, t("createInternalResourceDialogNameRequired")) - .max(255, t("createInternalResourceDialogNameMaxLength")), - siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), - protocol: z.enum(["tcp", "udp"]), - proxyPort: z - .number() - .int() - .positive() - .min(1, t("createInternalResourceDialogProxyPortMin")) - .max(65535, t("createInternalResourceDialogProxyPortMax")), - destinationIp: z.string(), - destinationPort: z - .number() - .int() - .positive() - .min(1, t("createInternalResourceDialogDestinationPortMin")) - .max(65535, t("createInternalResourceDialogDestinationPortMax")) - }); - - type FormData = z.infer; - - const availableSites = sites.filter( - (site) => site.type === "newt" && site.subnet - ); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - name: "", - siteId: availableSites[0]?.siteId || 0, - protocol: "tcp", - proxyPort: undefined, - destinationIp: "", - destinationPort: undefined - } - }); - - useEffect(() => { - if (open && availableSites.length > 0) { - form.reset({ - name: "", - siteId: availableSites[0].siteId, - protocol: "tcp", - proxyPort: undefined, - destinationIp: "", - destinationPort: undefined - }); - } - }, [open]); - - const handleSubmit = async (data: FormData) => { - setIsSubmitting(true); - try { - await api.put(`/org/${orgId}/site/${data.siteId}/resource`, { - name: data.name, - protocol: data.protocol, - proxyPort: data.proxyPort, - destinationIp: data.destinationIp, - destinationPort: data.destinationPort, - enabled: true - }); - - toast({ - title: t("createInternalResourceDialogSuccess"), - description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"), - variant: "default" - }); - - onSuccess?.(); - setOpen(false); - } catch (error) { - console.error("Error creating internal resource:", error); - toast({ - title: t("createInternalResourceDialogError"), - description: formatAxiosError( - error, - t("createInternalResourceDialogFailedToCreateInternalResource") - ), - variant: "destructive" - }); - } finally { - setIsSubmitting(false); - } - }; - - if (availableSites.length === 0) { - return ( - - - - {t("createInternalResourceDialogNoSitesAvailable")} - - {t("createInternalResourceDialogNoSitesAvailableDescription")} - - - - - - - - ); - } - - return ( - - - - {t("createInternalResourceDialogCreateClientResource")} - - {t("createInternalResourceDialogCreateClientResourceDescription")} - - - -
- - {/* Resource Properties Form */} -
-

- {t("createInternalResourceDialogResourceProperties")} -

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

- {t("createInternalResourceDialogTargetConfiguration")} -

-
-
- ( - - - {t("targetAddr")} - - - - - - {t("createInternalResourceDialogDestinationIPDescription")} - - - - )} - /> - - ( - - - {t("targetPort")} - - - - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } - /> - - - {t("createInternalResourceDialogDestinationPortDescription")} - - - - )} - /> -
-
-
-
- -
- - - - -
-
- ); -} diff --git a/src/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx index af0a0fe6..d909b7ea 100644 --- a/src/components/DataTablePagination.tsx +++ b/src/components/DataTablePagination.tsx @@ -14,42 +14,29 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { useTranslations } from "next-intl"; interface DataTablePaginationProps { table: Table; - onPageSizeChange?: (pageSize: number) => void; } export function DataTablePagination({ - table, - onPageSizeChange + table }: DataTablePaginationProps) { - const t = useTranslations(); - - const handlePageSizeChange = (value: string) => { - const newPageSize = Number(value); - table.setPageSize(newPageSize); - - // Call the callback if provided (for persistence) - if (onPageSizeChange) { - onPageSizeChange(newPageSize); - } - }; - return (
( - {t('otpSetupSecretCode')} + Authenticator Code @@ -171,17 +168,19 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) { size={48} />

- {t('otpRemoveSuccess')} + Two-Factor Authentication Disabled

- {t('otpRemoveSuccessMessage')} + Two-factor authentication has been disabled for + your account. You can enable it again at any + time.

)} - + {step === "password" && ( )} diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx deleted file mode 100644 index 66267bdf..00000000 --- a/src/components/DomainPicker.tsx +++ /dev/null @@ -1,837 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@/components/ui/popover"; -import { - AlertCircle, - CheckCircle2, - Building2, - Zap, - Check, - ChevronsUpDown, - ArrowUpDown -} from "lucide-react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { - sanitizeInputRaw, - finalizeSubdomainSanitize, - validateByDomainType, - isValidSubdomainStructure -} from "@/lib/subdomain-utils"; -import { toUnicode } from "punycode"; - -type OrganizationDomain = { - domainId: string; - baseDomain: string; - verified: boolean; - type: "ns" | "cname" | "wildcard"; -}; - -type AvailableOption = { - domainNamespaceId: string; - fullDomain: string; - domainId: string; -}; - -type DomainOption = { - id: string; - domain: string; - type: "organization" | "provided" | "provided-search"; - verified?: boolean; - domainType?: "ns" | "cname" | "wildcard"; - domainId?: string; - domainNamespaceId?: string; -}; - -interface DomainPicker2Props { - orgId: string; - onDomainChange?: (domainInfo: { - domainId: string; - domainNamespaceId?: string; - type: "organization" | "provided"; - subdomain?: string; - fullDomain: string; - baseDomain: string; - }) => void; - cols?: number; - hideFreeDomain?: boolean; -} - -export default function DomainPicker2({ - orgId, - onDomainChange, - cols = 2, - hideFreeDomain = false -}: DomainPicker2Props) { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const t = useTranslations(); - - if (!env.flags.usePangolinDns) { - hideFreeDomain = true; - } - - const [subdomainInput, setSubdomainInput] = useState(""); - const [selectedBaseDomain, setSelectedBaseDomain] = - useState(null); - const [availableOptions, setAvailableOptions] = useState( - [] - ); - const [organizationDomains, setOrganizationDomains] = useState< - OrganizationDomain[] - >([]); - const [loadingDomains, setLoadingDomains] = useState(false); - const [open, setOpen] = useState(false); - - // Provided domain search states - const [userInput, setUserInput] = useState(""); - const [isChecking, setIsChecking] = useState(false); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - const [providedDomainsShown, setProvidedDomainsShown] = useState(3); - const [selectedProvidedDomain, setSelectedProvidedDomain] = - useState(null); - - useEffect(() => { - const loadOrganizationDomains = async () => { - setLoadingDomains(true); - try { - const response = await api.get< - AxiosResponse - >(`/org/${orgId}/domains`); - if (response.status === 200) { - const domains = response.data.data.domains - .filter( - (domain) => - domain.type === "ns" || - domain.type === "cname" || - domain.type === "wildcard" - ) - .map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - type: domain.type as "ns" | "cname" | "wildcard" - })); - setOrganizationDomains(domains); - - // Auto-select first available domain - if (domains.length > 0) { - // Select the first organization domain - const firstOrgDomain = domains[0]; - const domainOption: DomainOption = { - id: `org-${firstOrgDomain.domainId}`, - domain: firstOrgDomain.baseDomain, - type: "organization", - verified: firstOrgDomain.verified, - domainType: firstOrgDomain.type, - domainId: firstOrgDomain.domainId - }; - setSelectedBaseDomain(domainOption); - - onDomainChange?.({ - domainId: firstOrgDomain.domainId, - type: "organization", - subdomain: undefined, - fullDomain: firstOrgDomain.baseDomain, - baseDomain: firstOrgDomain.baseDomain - }); - }/* else if ( - (build === "saas" || build === "enterprise") && - !hideFreeDomain - ) { - // If no organization domains, select the provided domain option - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - const freeDomainOption: DomainOption = { - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }; - setSelectedBaseDomain(freeDomainOption); - }*/ - } - } catch (error) { - console.error("Failed to load organization domains:", error); - toast({ - variant: "destructive", - title: t("domainPickerError"), - description: t("domainPickerErrorLoadDomains") - }); - } finally { - setLoadingDomains(false); - } - }; - - loadOrganizationDomains(); - }, [orgId, api, hideFreeDomain]); - - const checkAvailability = useCallback( - async (input: string) => { - if (!input.trim()) { - setAvailableOptions([]); - setIsChecking(false); - return; - } - - setIsChecking(true); - try { - const checkSubdomain = input - .toLowerCase() - .replace(/\./g, "-") - .replace(/[^a-z0-9-]/g, "") - .replace(/-+/g, "-") // 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([]); - toast({ - variant: "destructive", - title: t("domainPickerError"), - description: t("domainPickerErrorCheckAvailability") - }); - } finally { - setIsChecking(false); - } - }, - [api] - ); - - const debouncedCheckAvailability = useCallback( - debounce(checkAvailability, 500), - [checkAvailability] - ); - - useEffect(() => { - if (selectedBaseDomain?.type === "provided-search") { - setProvidedDomainsShown(3); - setSelectedProvidedDomain(null); - - if (userInput.trim()) { - setIsChecking(true); - debouncedCheckAvailability(userInput); - } else { - setAvailableOptions([]); - setIsChecking(false); - } - } - }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); - - const generateDropdownOptions = (): DomainOption[] => { - const options: DomainOption[] = []; - - organizationDomains.forEach((orgDomain) => { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: orgDomain.type, - domainId: orgDomain.domainId - }); - }); - - /* if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - options.push({ - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }); - }*/ - - return options; - }; - - const dropdownOptions = generateDropdownOptions(); - - const finalizeSubdomain = (sub: string, base: DomainOption): string => { - const sanitized = finalizeSubdomainSanitize(sub); - - if (!sanitized) { - toast({ - variant: "destructive", - title: t("domainPickerInvalidSubdomain"), - description: t("domainPickerInvalidSubdomainRemoved", { sub }) - }); - return ""; - } - - const ok = validateByDomainType(sanitized, { - type: - base.type === "provided-search" - ? "provided-search" - : "organization", - domainType: base.domainType - }); - - if (!ok) { - toast({ - variant: "destructive", - title: t("domainPickerInvalidSubdomain"), - description: t("domainPickerInvalidSubdomainCannotMakeValid", { - sub, - domain: base.domain - }) - }); - return ""; - } - - if (sub !== sanitized) { - toast({ - title: t("domainPickerSubdomainSanitized"), - description: t("domainPickerSubdomainCorrected", { - sub, - sanitized - }) - }); - } - - return sanitized; - }; - - const handleSubdomainChange = (value: string) => { - const raw = sanitizeInputRaw(value); - setSubdomainInput(raw); - setSelectedProvidedDomain(null); - - if (selectedBaseDomain?.type === "organization") { - const fullDomain = raw - ? `${raw}.${selectedBaseDomain.domain}` - : selectedBaseDomain.domain; - - onDomainChange?.({ - domainId: selectedBaseDomain.domainId!, - type: "organization", - subdomain: raw || undefined, - fullDomain, - baseDomain: selectedBaseDomain.domain - }); - } - }; - - const handleProvidedDomainInputChange = (value: string) => { - setUserInput(value); - if (selectedProvidedDomain) { - setSelectedProvidedDomain(null); - onDomainChange?.({ - domainId: "", - type: "provided", - subdomain: undefined, - fullDomain: "", - baseDomain: "" - }); - } - }; - - const handleBaseDomainSelect = (option: DomainOption) => { - let sub = subdomainInput; - - if (sub && sub.trim() !== "") { - sub = finalizeSubdomain(sub, option) || ""; - setSubdomainInput(sub); - } else { - sub = ""; - setSubdomainInput(""); - } - - if (option.type === "provided-search") { - setUserInput(""); - setAvailableOptions([]); - setSelectedProvidedDomain(null); - } - - setSelectedBaseDomain(option); - setOpen(false); - - if (option.domainType === "cname") { - sub = ""; - setSubdomainInput(""); - } - - const fullDomain = sub ? `${sub}.${option.domain}` : option.domain; - - onDomainChange?.({ - domainId: option.domainId || "", - domainNamespaceId: option.domainNamespaceId, - type: - option.type === "provided-search" ? "provided" : "organization", - subdomain: sub || undefined, - fullDomain, - baseDomain: option.domain - }); - }; - - const handleProvidedDomainSelect = (option: AvailableOption) => { - setSelectedProvidedDomain(option); - - const parts = option.fullDomain.split("."); - const subdomain = parts[0]; - const baseDomain = parts.slice(1).join("."); - - onDomainChange?.({ - domainId: option.domainId, - domainNamespaceId: option.domainNamespaceId, - type: "provided", - subdomain, - fullDomain: option.fullDomain, - baseDomain - }); - }; - - const isSubdomainValid = - selectedBaseDomain && subdomainInput - ? validateByDomainType(subdomainInput, { - type: - selectedBaseDomain.type === "provided-search" - ? "provided-search" - : "organization", - domainType: selectedBaseDomain.domainType - }) - : true; - - const showSubdomainInput = - selectedBaseDomain && - selectedBaseDomain.type === "organization" && - selectedBaseDomain.domainType !== "cname"; - const showProvidedDomainSearch = - selectedBaseDomain?.type === "provided-search"; - - const sortedAvailableOptions = [...availableOptions].sort((a, b) => { - const comparison = a.fullDomain.localeCompare(b.fullDomain); - return sortOrder === "asc" ? comparison : -comparison; - }); - - const displayedProvidedOptions = sortedAvailableOptions.slice( - 0, - providedDomainsShown - ); - const hasMoreProvided = - sortedAvailableOptions.length > providedDomainsShown; - - return ( -
-
-
- - { - if (showProvidedDomainSearch) { - handleProvidedDomainInputChange(e.target.value); - } else { - handleSubdomainChange(e.target.value); - } - }} - /> - {showSubdomainInput && - subdomainInput && - !isValidSubdomainStructure(subdomainInput) && ( -

- {t("domainPickerInvalidSubdomainStructure")} -

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

- {t("domainPickerEnterSubdomainOrLeaveBlank")} -

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

- {t("domainPickerEnterSubdomainToSearch")} -

- )} -
- -
- - - - - - - - - -
- {t("domainPickerNoDomainsFound")} -
-
- - {organizationDomains.length > 0 && ( - <> - - - {organizationDomains.map( - (orgDomain) => ( - - handleBaseDomainSelect( - { - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: - orgDomain.verified, - domainType: - orgDomain.type, - domainId: - orgDomain.domainId - } - ) - } - className="mx-2 rounded-md" - disabled={ - !orgDomain.verified - } - > -
- -
-
- - { - orgDomain.baseDomain - } - - - {orgDomain.type.toUpperCase()}{" "} - •{" "} - {orgDomain.verified - ? t( - "domainPickerVerified" - ) - : t( - "domainPickerUnverified" - )} - -
- -
- ) - )} -
-
- {/*(build === "saas" || - build === "enterprise") && - !hideFreeDomain && ( - - )*/} - - )} - - {/*(build === "saas" || build === "enterprise") && - !hideFreeDomain && ( - - - - handleBaseDomainSelect({ - id: "provided-search", - domain: - build === - "enterprise" - ? t( - "domainPickerProvidedDomain" - ) - : t( - "domainPickerFreeProvidedDomain" - ), - type: "provided-search" - }) - } - className="mx-2 rounded-md" - > -
- -
-
- - {build === - "enterprise" - ? t( - "domainPickerProvidedDomain" - ) - : t( - "domainPickerFreeProvidedDomain" - )} - - - {t( - "domainPickerSearchForAvailableDomains" - )} - -
- -
-
-
- )*/} -
-
-
-
-
- - {/*showProvidedDomainSearch && build === "saas" && ( - - - - {t("domainPickerNotWorkSelfHosted")} - - - )*/} - - {showProvidedDomainSearch && ( -
- {isChecking && ( -
-
-
- - {t("domainPickerCheckingAvailability")} - -
-
- )} - - {!isChecking && - sortedAvailableOptions.length === 0 && - userInput.trim() && ( - - - - {t("domainPickerNoMatchingDomains")} - - - )} - - {!isChecking && sortedAvailableOptions.length > 0 && ( -
- { - const option = - displayedProvidedOptions.find( - (opt) => - opt.domainNamespaceId === value - ); - if (option) { - handleProvidedDomainSelect(option); - } - }} - className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} - > - {displayedProvidedOptions.map((option) => ( - - ))} - - {hasMoreProvided && ( - - )} -
- )} -
- )} - - {loadingDomains && ( -
-
-
- {t("domainPickerLoadingDomains")} -
-
- )} -
- ); -} - -function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout | null = null; - - return (...args: Parameters) => { - if (timeout) clearTimeout(timeout); - - timeout = setTimeout(() => { - func(...args); - }, wait); - }; -} diff --git a/src/components/DomainsDataTable.tsx b/src/components/DomainsDataTable.tsx deleted file mode 100644 index 4059b7d3..00000000 --- a/src/components/DomainsDataTable.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; -import { useTranslations } from "next-intl"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - onAdd?: () => void; - onRefresh?: () => void; - isRefreshing?: boolean; -} - -export function DomainsDataTable({ - columns, - data, - onAdd, - onRefresh, - isRefreshing -}: DataTableProps) { - const t = useTranslations(); - - return ( - - ); -} diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx deleted file mode 100644 index 5bafe935..00000000 --- a/src/components/DomainsTable.tsx +++ /dev/null @@ -1,278 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DomainsDataTable } from "@app/components/DomainsDataTable"; -import { Button } from "@app/components/ui/button"; -import { ArrowUpDown } from "lucide-react"; -import { useState } from "react"; -import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Badge } from "@app/components/ui/badge"; -import { useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import CreateDomainForm from "@app/components/CreateDomainForm"; -import { useToast } from "@app/hooks/useToast"; -import { useOrgContext } from "@app/hooks/useOrgContext"; - -export type DomainRow = { - domainId: string; - baseDomain: string; - type: string; - verified: boolean; - failed: boolean; - tries: number; - configManaged: boolean; -}; - -type Props = { - domains: DomainRow[]; -}; - -export default function DomainsTable({ domains }: Props) { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [selectedDomain, setSelectedDomain] = useState( - null - ); - const [isRefreshing, setIsRefreshing] = useState(false); - const [restartingDomains, setRestartingDomains] = useState>( - new Set() - ); - const api = createApiClient(useEnvContext()); - const router = useRouter(); - const t = useTranslations(); - const { toast } = useToast(); - const { org } = useOrgContext(); - - const refreshData = async () => { - 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 deleteDomain = async (domainId: string) => { - try { - await api.delete(`/org/${org.org.orgId}/domain/${domainId}`); - toast({ - title: t("success"), - description: t("domainDeletedDescription") - }); - setIsDeleteModalOpen(false); - refreshData(); - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e), - variant: "destructive" - }); - } - }; - - const restartDomain = async (domainId: string) => { - setRestartingDomains((prev) => new Set(prev).add(domainId)); - try { - await api.post(`/org/${org.org.orgId}/domain/${domainId}/restart`); - toast({ - title: t("success"), - description: t("domainRestartedDescription", { - fallback: "Domain verification restarted successfully" - }) - }); - refreshData(); - } catch (e) { - toast({ - title: t("error"), - description: formatAxiosError(e), - variant: "destructive" - }); - } finally { - setRestartingDomains((prev) => { - const newSet = new Set(prev); - newSet.delete(domainId); - return newSet; - }); - } - }; - - const getTypeDisplay = (type: string) => { - switch (type) { - case "ns": - return t("selectDomainTypeNsName"); - case "cname": - return t("selectDomainTypeCnameName"); - case "wildcard": - return t("selectDomainTypeWildcardName"); - default: - return type; - } - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "baseDomain", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "type", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const type = row.original.type; - return ( - {getTypeDisplay(type)} - ); - } - }, - { - accessorKey: "verified", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const { verified, failed } = row.original; - if (verified) { - return {t("verified")}; - } else if (failed) { - return ( - - {t("failed", { fallback: "Failed" })} - - ); - } else { - return {t("pending")}; - } - } - }, - { - id: "actions", - cell: ({ row }) => { - const domain = row.original; - const isRestarting = restartingDomains.has(domain.domainId); - - return ( -
- {domain.failed && ( - - )} - -
- ); - } - } - ]; - - return ( - <> - {selectedDomain && ( - { - setIsDeleteModalOpen(val); - setSelectedDomain(null); - }} - dialog={ -
-

- {t("domainQuestionRemove", { - domain: selectedDomain.baseDomain - })} -

-

- {t("domainMessageRemove")} -

-

{t("domainMessageConfirm")}

-
- } - buttonText={t("domainConfirmDelete")} - onConfirm={async () => - deleteDomain(selectedDomain.domainId) - } - string={selectedDomain.baseDomain} - title={t("domainDelete")} - /> - )} - - { - refreshData(); - }} - /> - - setIsCreateModalOpen(true)} - onRefresh={refreshData} - isRefreshing={isRefreshing} - /> - - ); -} diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx deleted file mode 100644 index d09f0b6c..00000000 --- a/src/components/EditInternalResourceDialog.tsx +++ /dev/null @@ -1,276 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button } from "@app/components/ui/button"; -import { Input } from "@app/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { toast } from "@app/hooks/useToast"; -import { useTranslations } from "next-intl"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Separator } from "@app/components/ui/separator"; - -type InternalResourceData = { - id: number; - name: string; - orgId: string; - siteName: string; - protocol: string; - proxyPort: number | null; - siteId: number; - destinationIp?: string; - destinationPort?: number; -}; - -type EditInternalResourceDialogProps = { - open: boolean; - setOpen: (val: boolean) => void; - resource: InternalResourceData; - orgId: string; - onSuccess?: () => void; -}; - -export default function EditInternalResourceDialog({ - open, - setOpen, - resource, - orgId, - onSuccess -}: EditInternalResourceDialogProps) { - const t = useTranslations(); - const api = createApiClient(useEnvContext()); - const [isSubmitting, setIsSubmitting] = useState(false); - - const formSchema = z.object({ - name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), - protocol: z.enum(["tcp", "udp"]), - proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), - destinationIp: z.string(), - destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")) - }); - - type FormData = z.infer; - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - name: resource.name, - protocol: resource.protocol as "tcp" | "udp", - proxyPort: resource.proxyPort || undefined, - destinationIp: resource.destinationIp || "", - destinationPort: resource.destinationPort || undefined - } - }); - - useEffect(() => { - if (open) { - form.reset({ - name: resource.name, - protocol: resource.protocol as "tcp" | "udp", - proxyPort: resource.proxyPort || undefined, - destinationIp: resource.destinationIp || "", - destinationPort: resource.destinationPort || undefined - }); - } - }, [open, resource, form]); - - const handleSubmit = async (data: FormData) => { - setIsSubmitting(true); - try { - // Update the site resource - await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { - name: data.name, - protocol: data.protocol, - proxyPort: data.proxyPort, - destinationIp: data.destinationIp, - destinationPort: data.destinationPort - }); - - toast({ - title: t("editInternalResourceDialogSuccess"), - description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"), - variant: "default" - }); - - onSuccess?.(); - setOpen(false); - } catch (error) { - console.error("Error updating internal resource:", error); - toast({ - title: t("editInternalResourceDialogError"), - description: formatAxiosError(error, t("editInternalResourceDialogFailedToUpdateInternalResource")), - variant: "destructive" - }); - } finally { - setIsSubmitting(false); - } - }; - - return ( - - - - {t("editInternalResourceDialogEditClientResource")} - - {t("editInternalResourceDialogUpdateResourceProperties", { resourceName: resource.name })} - - - -
- - {/* Resource Properties Form */} -
-

{t("editInternalResourceDialogResourceProperties")}

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

{t("editInternalResourceDialogTargetConfiguration")}

-
-
- ( - - {t("targetAddr")} - - - - - - )} - /> - - ( - - {t("targetPort")} - - field.onChange(parseInt(e.target.value) || 0)} - /> - - - - )} - /> -
-
-
-
- -
- - - - -
-
- ); -} diff --git a/src/components/Enable2FaDialog.tsx b/src/components/Enable2FaDialog.tsx deleted file mode 100644 index 0fca0085..00000000 --- a/src/components/Enable2FaDialog.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useState, useRef } from "react"; -import { Button } from "@/components/ui/button"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import TwoFactorSetupForm from "./TwoFactorSetupForm"; -import { useTranslations } from "next-intl"; -import { useUserContext } from "@app/hooks/useUserContext"; - -type Enable2FaDialogProps = { - open: boolean; - setOpen: (val: boolean) => void; -}; - -export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps) { - const t = useTranslations(); - const [currentStep, setCurrentStep] = useState(1); - const [loading, setLoading] = useState(false); - const formRef = useRef<{ handleSubmit: () => void }>(null); - const { user, updateUser } = useUserContext(); - - function reset() { - setCurrentStep(1); - setLoading(false); - } - - const handleSubmit = () => { - if (formRef.current) { - formRef.current.handleSubmit(); - } - }; - - return ( - { - setOpen(val); - reset(); - }} - > - - - - {t('otpSetup')} - - - {t('otpSetupDescription')} - - - - {setOpen(false); updateUser({ twoFactorEnabled: true });}} - onStepChange={setCurrentStep} - onLoadingChange={setLoading} - /> - - - - - - {(currentStep === 1 || currentStep === 2) && ( - - )} - - - - ); -} \ No newline at end of file diff --git a/src/components/Enable2FaForm.tsx b/src/components/Enable2FaForm.tsx index acc00400..dcc10d58 100644 --- a/src/components/Enable2FaForm.tsx +++ b/src/components/Enable2FaForm.tsx @@ -1,7 +1,53 @@ "use client"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, CheckCircle2 } from "lucide-react"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { AxiosResponse } from "axios"; +import { + RequestTotpSecretBody, + RequestTotpSecretResponse, + VerifyTotpBody, + VerifyTotpResponse +} from "@server/routers/auth"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { useUserContext } from "@app/hooks/useUserContext"; -import Enable2FaDialog from "./Enable2FaDialog"; + +const enableSchema = z.object({ + password: z.string().min(1, { message: "Password is required" }) +}); + +const confirmSchema = z.object({ + code: z.string().length(6, { message: "Invalid code" }) +}); type Enable2FaProps = { open: boolean; @@ -9,5 +55,254 @@ type Enable2FaProps = { }; export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) { - return ; + const [step, setStep] = useState(1); + const [secretKey, setSecretKey] = useState(""); + const [secretUri, setSecretUri] = useState(""); + const [verificationCode, setVerificationCode] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + const [loading, setLoading] = useState(false); + const [backupCodes, setBackupCodes] = useState([]); + + const { user, updateUser } = useUserContext(); + + const api = createApiClient(useEnvContext()); + + const enableForm = useForm>({ + resolver: zodResolver(enableSchema), + defaultValues: { + password: "" + } + }); + + const confirmForm = useForm>({ + resolver: zodResolver(confirmSchema), + defaultValues: { + code: "" + } + }); + + const request2fa = async (values: z.infer) => { + setLoading(true); + + const res = await api + .post>( + `/auth/2fa/request`, + { + password: values.password + } as RequestTotpSecretBody + ) + .catch((e) => { + toast({ + title: "Unable to enable 2FA", + description: formatAxiosError( + e, + "An error occurred while enabling 2FA" + ), + variant: "destructive" + }); + }); + + if (res && res.data.data.secret) { + setSecretKey(res.data.data.secret); + setSecretUri(res.data.data.uri); + setStep(2); + } + + setLoading(false); + }; + + const confirm2fa = async (values: z.infer) => { + setLoading(true); + + const res = await api + .post>(`/auth/2fa/enable`, { + code: values.code + } as VerifyTotpBody) + .catch((e) => { + toast({ + title: "Unable to enable 2FA", + description: formatAxiosError( + e, + "An error occurred while enabling 2FA" + ), + variant: "destructive" + }); + }); + + if (res && res.data.data.valid) { + setBackupCodes(res.data.data.backupCodes || []); + updateUser({ twoFactorEnabled: true }); + setStep(3); + } + + setLoading(false); + }; + + const handleVerify = () => { + if (verificationCode.length !== 6) { + setError("Please enter a 6-digit code"); + return; + } + if (verificationCode === "123456") { + setSuccess(true); + setStep(3); + } else { + setError("Invalid code. Please try again."); + } + }; + + function reset() { + setLoading(false); + setStep(1); + setSecretKey(""); + setSecretUri(""); + setVerificationCode(""); + setError(""); + setSuccess(false); + setBackupCodes([]); + enableForm.reset(); + confirmForm.reset(); + } + + return ( + { + setOpen(val); + reset(); + }} + > + + + + Enable Two-factor Authentication + + + Secure your account with an extra layer of protection + + + + {step === 1 && ( +
+ +
+ ( + + Password + + + + + + )} + /> +
+
+ + )} + + {step === 2 && ( +
+

+ Scan this QR code with your authenticator app or + enter the secret key manually: +

+
+ +
+
+ +
+ +
+ +
+ ( + + + Authenticator Code + + + + + + + )} + /> +
+
+ +
+ )} + + {step === 3 && ( +
+ +

+ Two-Factor Authentication Enabled +

+

+ Your account is now more secure. Don't forget to + save your backup codes. +

+ +
+ +
+
+ )} +
+ + + + + {(step === 1 || step === 2) && ( + + )} + +
+
+ ); } diff --git a/src/components/GenerateLicenseKeyForm.tsx b/src/components/GenerateLicenseKeyForm.tsx deleted file mode 100644 index 9cbddd70..00000000 --- a/src/components/GenerateLicenseKeyForm.tsx +++ /dev/null @@ -1,1084 +0,0 @@ -"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 deleted file mode 100644 index 835bb70d..00000000 --- a/src/components/GenerateLicenseKeysTable.tsx +++ /dev/null @@ -1,203 +0,0 @@ -"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/HeadersInput.tsx b/src/components/HeadersInput.tsx deleted file mode 100644 index 4a93ddc8..00000000 --- a/src/components/HeadersInput.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { useEffect, useState, useRef } from "react"; -import { Textarea } from "@/components/ui/textarea"; - - -interface HeadersInputProps { - value?: { name: string, value: string }[] | null; - onChange: (value: { name: string, value: string }[] | null) => void; - placeholder?: string; - rows?: number; - className?: string; -} - -export function HeadersInput({ - value = [], - onChange, - placeholder = `X-Example-Header: example-value -X-Another-Header: another-value`, - rows = 4, - className -}: HeadersInputProps) { - const [internalValue, setInternalValue] = useState(""); - const textareaRef = useRef(null); - const isUserEditingRef = useRef(false); - - // Convert header objects array to newline-separated string for display - const convertToNewlineSeparated = (headers: { name: string, value: string }[] | null): string => { - if (!headers || headers.length === 0) return ""; - - return headers - .map(header => `${header.name}: ${header.value}`) - .join('\n'); - }; - - // Convert newline-separated string to header objects array - const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => { - if (!newlineSeparated || newlineSeparated.trim() === "") return []; - - return newlineSeparated - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0 && line.includes(':')) - .map(line => { - const colonIndex = line.indexOf(':'); - const name = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - - // Ensure header name conforms to HTTP header requirements - // Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens - const normalizedName = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase(); - - return { name: normalizedName, value }; - }) - .filter(header => header.name.length > 0); // Filter out headers with invalid names - }; - - // Update internal value when external value changes - // But only if the user is not currently editing (textarea not focused) - useEffect(() => { - if (!isUserEditingRef.current) { - setInternalValue(convertToNewlineSeparated(value)); - } - }, [value]); - - const handleChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - setInternalValue(newValue); - - // Mark that user is actively editing - isUserEditingRef.current = true; - - // Only update parent if the input is in a valid state - // Valid states: empty/whitespace only, or contains properly formatted headers - - if (newValue.trim() === "") { - // Empty input is valid - represents no headers - onChange([]); - } else { - // Check if all non-empty lines are properly formatted (contain ':') - const lines = newValue.split('\n'); - const nonEmptyLines = lines - .map(line => line.trim()) - .filter(line => line.length > 0); - - // If there are no non-empty lines, or all non-empty lines contain ':', it's valid - const isValid = nonEmptyLines.length === 0 || nonEmptyLines.every(line => line.includes(':')); - - if (isValid) { - // Safe to convert and update parent - const headersArray = convertToHeadersArray(newValue); - onChange(headersArray); - } - // If not valid, don't call onChange - let user continue typing - } - }; - - const handleFocus = () => { - isUserEditingRef.current = true; - }; - - const handleBlur = () => { - // Small delay to allow any final change events to process - setTimeout(() => { - isUserEditingRef.current = false; - }, 100); - }; - - return ( -