mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 10:56:38 +00:00
Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af1e0ef56 | ||
|
|
08b7d6735c | ||
|
|
a91ebd1e91 | ||
|
|
312e03b4eb | ||
|
|
e8a57e432c | ||
|
|
bca2eef2e8 | ||
|
|
ec7211a15d | ||
|
|
46807c6477 | ||
|
|
b578786e62 | ||
|
|
2e0ad8d262 | ||
|
|
003f0cfa6d | ||
|
|
ee3df081ef | ||
|
|
08eeb12519 | ||
|
|
e66c6b2505 | ||
|
|
d2a880d9c8 | ||
|
|
edc0b86470 | ||
|
|
aebe6b80b7 | ||
|
|
4d87333b43 | ||
|
|
ef32f3ed5a | ||
|
|
216ded3034 | ||
|
|
cb59fe2cee | ||
|
|
7776f6d09c | ||
|
|
c50392c947 | ||
|
|
ceee978fcd | ||
|
|
c5a73dc87e | ||
|
|
7198ef2774 | ||
|
|
7e9a066797 | ||
|
|
ba96332313 | ||
|
|
e2d0338b0b | ||
|
|
59ecab5738 | ||
|
|
721bf3403d | ||
|
|
3b8ba47377 | ||
|
|
e752929f69 | ||
|
|
e41c3e6f54 | ||
|
|
9dedd1a8de | ||
|
|
c4a5fae28f | ||
|
|
5f95a3233f | ||
|
|
d3174d0196 | ||
|
|
3710d71974 | ||
|
|
f62e88eb67 | ||
|
|
904b302fb6 | ||
|
|
5fc096f2d5 | ||
|
|
87668c492f | ||
|
|
6d7a8b97ad | ||
|
|
282d444933 | ||
|
|
f3d7d97fb9 | ||
|
|
de857a7c4e | ||
|
|
20a0ebfc9d | ||
|
|
ba8166bdeb | ||
|
|
2b634fc6c5 | ||
|
|
5429bc03ab | ||
|
|
a558b34608 | ||
|
|
1850d56977 | ||
|
|
61b4c62824 | ||
|
|
10e5ccfe86 | ||
|
|
9f5d475e80 | ||
|
|
9bb9a3acbe | ||
|
|
0923b7e3c5 | ||
|
|
ccd81f6fe2 | ||
|
|
0f74107e86 | ||
|
|
8377434c08 | ||
|
|
1fbf2bfb8d | ||
|
|
42facf8e12 | ||
|
|
4bb3d85c25 | ||
|
|
c0039190bd | ||
|
|
a8d00a47cd | ||
|
|
57bcbf6c48 | ||
|
|
c57db1479e | ||
|
|
cd8062ada3 | ||
|
|
244d05adb1 | ||
|
|
812bd64325 | ||
|
|
276d1361ac | ||
|
|
881eac4722 | ||
|
|
2a2a550a6a | ||
|
|
e75001080a | ||
|
|
6fbba38a76 | ||
|
|
902b413881 | ||
|
|
8b2f8ad3ef | ||
|
|
377cb77307 | ||
|
|
733bf0b169 | ||
|
|
8faff3e075 | ||
|
|
48af91c976 | ||
|
|
6664efaa13 | ||
|
|
e5ee96cf52 | ||
|
|
38faf1f905 | ||
|
|
2cff142266 | ||
|
|
2c99cfacc0 | ||
|
|
0c63ea1f50 | ||
|
|
f50df66e3a | ||
|
|
4b93491160 | ||
|
|
19210cbf7d | ||
|
|
9af206b69a | ||
|
|
b6b9c71c5e | ||
|
|
c000c4502f | ||
|
|
b6c1d9a592 | ||
|
|
7a75fe0cad | ||
|
|
a83e660902 | ||
|
|
65eb3e4b95 | ||
|
|
093fb419f3 | ||
|
|
026e56aead | ||
|
|
fa9bc59f62 | ||
|
|
06ec80db42 | ||
|
|
24d564b79b | ||
|
|
2f5e6248cd | ||
|
|
c0cc81ed96 | ||
|
|
b33a54a449 | ||
|
|
94137e587c | ||
|
|
a6086d3724 | ||
|
|
0a377150e3 | ||
|
|
d20e0a228a | ||
|
|
ca146a1b57 | ||
|
|
c7c3e3ee73 | ||
|
|
cd27f6459c | ||
|
|
b1e212721e | ||
|
|
ccd2773331 | ||
|
|
cfa82b51fb | ||
|
|
9c91a8db46 | ||
|
|
b160eee8d2 | ||
|
|
37ceabdf5d | ||
|
|
e7828a43fa | ||
|
|
ccb1f04ad8 | ||
|
|
4c14ccbb63 | ||
|
|
25c24ca9cf | ||
|
|
787869fe21 | ||
|
|
b51c27a823 | ||
|
|
5917881b47 | ||
|
|
c7a40d59b7 | ||
|
|
a50c0d84e9 | ||
|
|
f17a957058 | ||
|
|
2c63851130 | ||
|
|
6b125bba7c | ||
|
|
d92b87b7c8 | ||
|
|
f64a477c3d | ||
|
|
b6f8ed1e4a | ||
|
|
bad88e4741 | ||
|
|
01db519691 | ||
|
|
e601038c0f | ||
|
|
e0996a17ef | ||
|
|
526307e192 | ||
|
|
1b01c4f053 | ||
|
|
a184e23f16 | ||
|
|
06156e0ca6 | ||
|
|
02b1de3266 | ||
|
|
c5b3d92466 | ||
|
|
186a78b064 | ||
|
|
9a808dc139 | ||
|
|
977404b8c3 | ||
|
|
b00143ce9b | ||
|
|
4435d9a248 | ||
|
|
7d0303e2be | ||
|
|
a0da9c1129 | ||
|
|
5e73690570 | ||
|
|
b0409b7d52 | ||
|
|
fe474b3989 | ||
|
|
5154d5d3ee | ||
|
|
62df92f63a | ||
|
|
e2534af40e | ||
|
|
2ee3f10e02 | ||
|
|
5a3bf2f758 | ||
|
|
e121dd0d1d | ||
|
|
2c46a37a53 | ||
|
|
23f05d7f4e | ||
|
|
6105eea7a9 | ||
|
|
850e9a734a | ||
|
|
cb7c57fd03 | ||
|
|
494d0f7c14 | ||
|
|
a4e480e02b | ||
|
|
cd285cc019 | ||
|
|
9e8e00d4bb | ||
|
|
389834f735 | ||
|
|
b14ddc07fb |
@@ -29,4 +29,6 @@ CONTRIBUTING.md
|
|||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
migrations/
|
migrations/
|
||||||
config/
|
config/
|
||||||
|
build.ts
|
||||||
|
tsconfig.json
|
||||||
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Build and Release
|
name: Build and Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: amd64-runner
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,4 +47,6 @@ server/db/index.ts
|
|||||||
server/build.ts
|
server/build.ts
|
||||||
postgres/
|
postgres/
|
||||||
dynamic/
|
dynamic/
|
||||||
*.mmdb
|
*.mmdb
|
||||||
|
scratch/
|
||||||
|
tsconfig.json
|
||||||
24
Dockerfile
24
Dockerfile
@@ -15,9 +15,29 @@ RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
|||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
||||||
|
|
||||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
# Copy the appropriate TypeScript configuration based on build type
|
||||||
|
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||||
|
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
||||||
|
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if the build is oss then remove the server/private directory
|
||||||
|
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
||||||
|
|
||||||
|
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 mkdir -p dist
|
||||||
|
RUN npm run next:build
|
||||||
|
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||||
|
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||||
|
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||||
|
else \
|
||||||
|
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# test to make sure the build output is there and error if not
|
||||||
|
RUN test -f dist/server.mjs
|
||||||
|
|
||||||
RUN npm run build:$DATABASE
|
|
||||||
RUN npm run build:cli
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
|
|||||||
20
Makefile
20
Makefile
@@ -8,6 +8,7 @@ build-release:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
@@ -16,6 +17,7 @@ build-release:
|
|||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
@@ -23,6 +25,24 @@ build-release:
|
|||||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-latest \
|
||||||
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
--push .
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
|
--push .
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
|
|||||||
141
README.md
141
README.md
@@ -1,46 +1,36 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h2>
|
<h2>
|
||||||
<picture>
|
<a href="https://digpangolin.com">
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
<picture>
|
||||||
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
|
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||||
|
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="350">
|
||||||
</picture>
|
</picture>
|
||||||
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 align="center">Secure gateway to your private networks</h4>
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h5>
|
<h5>
|
||||||
<a href="https://digpangolin.com">
|
<a href="https://digpangolin.com">
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
<a href="https://docs.digpangolin.com/">
|
||||||
Quick Install Guide
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="mailto:contact@fossorial.io">
|
<a href="mailto:contact@fossorial.io">
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
|
||||||
<a href="https://digpangolin.com/slack">
|
|
||||||
Slack
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="https://discord.gg/HCJR8Xhme4">
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</h5>
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://discord.gg/HCJR8Xhme4)
|
||||||
[](https://digpangolin.com/slack)
|
[](https://digpangolin.com/slack)
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||

|

|
||||||
[](https://discord.gg/HCJR8Xhme4)
|
|
||||||
[](https://www.youtube.com/@fossorial-app)
|
[](https://www.youtube.com/@fossorial-app)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -51,107 +41,48 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
|||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<img src="public/screenshots/hero.png" alt="Preview"/>
|
## Installation
|
||||||
|
|
||||||

|
Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick-install) for how to install and set up Pangolin.
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Reverse Proxy Through WireGuard Tunnel
|
|
||||||
|
|
||||||
- Expose private resources on your network **without opening ports** (firewall punching).
|
|
||||||
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
|
||||||
- Built-in support for any WireGuard client.
|
|
||||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
|
||||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
|
||||||
- Load balancing.
|
|
||||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
|
||||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
|
||||||
- Attach as many sites to the central server as you wish.
|
|
||||||
|
|
||||||
### Identity & Access Management
|
|
||||||
|
|
||||||
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
|
|
||||||
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
|
|
||||||
- TOTP with backup codes for two-factor authentication.
|
|
||||||
- Create organizations, each with multiple sites, users, and roles.
|
|
||||||
- **Role-based access control** to manage resource access permissions.
|
|
||||||
- Additional authentication options include:
|
|
||||||
- Email whitelisting with **one-time passcodes.**
|
|
||||||
- **Temporary, self-destructing share links.**
|
|
||||||
- Resource specific pin codes.
|
|
||||||
- Resource specific passwords.
|
|
||||||
- Passkeys
|
|
||||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
|
||||||
- Auto-provision users and roles from your IdP.
|
|
||||||
|
|
||||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### Manage Access to Internal Apps
|
|
||||||
|
|
||||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
|
||||||
|
|
||||||
### Developers and DevOps
|
|
||||||
|
|
||||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
|
||||||
|
|
||||||
### Secure API Gateway
|
|
||||||
|
|
||||||
- One application load balancer across multiple clouds and on-premises.
|
|
||||||
|
|
||||||
### IoT and Edge Devices
|
|
||||||
|
|
||||||
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
|
||||||
|
|
||||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
|
||||||
|
|
||||||
## Deployment Options
|
## Deployment Options
|
||||||
|
|
||||||
### Fully Self Hosted
|
| <img width=500 /> | Description |
|
||||||
|
|-----------------|--------------|
|
||||||
|
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||||
|
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||||
|
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. |
|
||||||
|
|
||||||
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
|
## Key Features
|
||||||
|
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||||
|
|
||||||
### Pangolin Cloud
|
| <img width=500 /> | <img width=500 /> |
|
||||||
|
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||||
|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||||
|
| **Reverse proxy across networks anywhere**<br /><br />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. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||||
|
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||||
|
| **Quickly connect Pangolin sites**<br /><br />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. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||||
|
|
||||||
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
## Get Started
|
||||||
|
|
||||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
### Check out the docs
|
||||||
|
|
||||||
### Managed & High Availability
|
We encourage everyone to read the full documentation first, which is
|
||||||
|
available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of
|
||||||
|
the docs to illustrate some basic ideas.
|
||||||
|
|
||||||
Managed control plane, your infrastructure
|
### Sign up and try now
|
||||||
|
|
||||||
- We manage database and control plane.
|
For Pangolin's managed service, you will first need to create an account at
|
||||||
- You self-host lightweight exit-node.
|
[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started.
|
||||||
- Traffic flows through your infra.
|
|
||||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
|
||||||
|
|
||||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
|
||||||
|
|
||||||
### Full Enterprise On-Premises
|
|
||||||
|
|
||||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
|
||||||
|
|
||||||
## Project Development / Roadmap
|
|
||||||
|
|
||||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please 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.
|
|
||||||
|
|
||||||
If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`.
|
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
let schema;
|
const schema = [
|
||||||
if (build === "oss") {
|
path.join("server", "db", "pg", "schema"),
|
||||||
schema = [path.join("server", "db", "pg", "schema.ts")];
|
];
|
||||||
} else {
|
|
||||||
schema = [
|
|
||||||
path.join("server", "db", "pg", "schema.ts"),
|
|
||||||
path.join("server", "db", "pg", "privateSchema.ts")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { build } from "@server/build";
|
|
||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
let schema;
|
const schema = [
|
||||||
if (build === "oss") {
|
path.join("server", "db", "sqlite", "schema"),
|
||||||
schema = [path.join("server", "db", "sqlite", "schema.ts")];
|
];
|
||||||
} else {
|
|
||||||
schema = [
|
|
||||||
path.join("server", "db", "sqlite", "schema.ts"),
|
|
||||||
path.join("server", "db", "sqlite", "privateSchema.ts")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
|
|||||||
210
esbuild.mjs
210
esbuild.mjs
@@ -2,8 +2,9 @@ import esbuild from "esbuild";
|
|||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
// import { glob } from "glob";
|
// import { glob } from "glob";
|
||||||
// import path from "path";
|
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
// patch __dirname
|
// patch __dirname
|
||||||
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const argv = yargs(hideBin(process.argv))
|
const argv = yargs(hideBin(process.argv))
|
||||||
.usage("Usage: $0 -entry [string] -out [string]")
|
.usage("Usage: $0 -entry [string] -out [string] -build [string]")
|
||||||
.option("entry", {
|
.option("entry", {
|
||||||
alias: "e",
|
alias: "e",
|
||||||
describe: "Entry point file",
|
describe: "Entry point file",
|
||||||
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
|
|||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
})
|
})
|
||||||
|
.option("build", {
|
||||||
|
alias: "b",
|
||||||
|
describe: "Build type (oss, saas, enterprise)",
|
||||||
|
type: "string",
|
||||||
|
choices: ["oss", "saas", "enterprise"],
|
||||||
|
default: "oss",
|
||||||
|
})
|
||||||
.help()
|
.help()
|
||||||
.alias("help", "h").argv;
|
.alias("help", "h").argv;
|
||||||
|
|
||||||
@@ -46,6 +54,179 @@ function getPackagePaths() {
|
|||||||
return ["package.json"];
|
return ["package.json"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin to guard against bad imports from #private
|
||||||
|
function privateImportGuardPlugin() {
|
||||||
|
return {
|
||||||
|
name: "private-import-guard",
|
||||||
|
setup(build) {
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#private\// }, (args) => {
|
||||||
|
const importingFile = args.importer;
|
||||||
|
|
||||||
|
// Check if the importing file is NOT in server/private
|
||||||
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
|
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||||
|
|
||||||
|
if (!isInServerPrivate) {
|
||||||
|
const violation = {
|
||||||
|
file: importingFile,
|
||||||
|
importPath: args.path,
|
||||||
|
resolveDir: args.resolveDir
|
||||||
|
};
|
||||||
|
violations.push(violation);
|
||||||
|
|
||||||
|
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||||
|
console.log(` File: ${importingFile}`);
|
||||||
|
console.log(` Import: ${args.path}`);
|
||||||
|
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null to let the default resolver handle it
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
||||||
|
violations.forEach((v, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
result.errors.push({
|
||||||
|
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||||
|
location: null,
|
||||||
|
notes: violations.map(v => ({
|
||||||
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
|
location: null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin to guard against bad imports from #private
|
||||||
|
function dynamicImportGuardPlugin() {
|
||||||
|
return {
|
||||||
|
name: "dynamic-import-guard",
|
||||||
|
setup(build) {
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||||
|
const importingFile = args.importer;
|
||||||
|
|
||||||
|
// Check if the importing file is NOT in server/private
|
||||||
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
|
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||||
|
|
||||||
|
if (isInServerPrivate) {
|
||||||
|
const violation = {
|
||||||
|
file: importingFile,
|
||||||
|
importPath: args.path,
|
||||||
|
resolveDir: args.resolveDir
|
||||||
|
};
|
||||||
|
violations.push(violation);
|
||||||
|
|
||||||
|
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||||
|
console.log(` File: ${importingFile}`);
|
||||||
|
console.log(` Import: ${args.path}`);
|
||||||
|
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null to let the default resolver handle it
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
||||||
|
violations.forEach((v, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
result.errors.push({
|
||||||
|
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||||
|
location: null,
|
||||||
|
notes: violations.map(v => ({
|
||||||
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
|
location: null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin to dynamically switch imports based on build type
|
||||||
|
function dynamicImportSwitcherPlugin(buildValue) {
|
||||||
|
return {
|
||||||
|
name: "dynamic-import-switcher",
|
||||||
|
setup(build) {
|
||||||
|
const switches = [];
|
||||||
|
|
||||||
|
build.onStart(() => {
|
||||||
|
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||||
|
// Extract the path after #dynamic/
|
||||||
|
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
||||||
|
|
||||||
|
// Determine the replacement based on build type
|
||||||
|
let replacement;
|
||||||
|
if (buildValue === "oss") {
|
||||||
|
replacement = `#open/${dynamicPath}`;
|
||||||
|
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
||||||
|
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
||||||
|
replacement = `#open/${dynamicPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchInfo = {
|
||||||
|
file: args.importer,
|
||||||
|
originalPath: args.path,
|
||||||
|
replacementPath: replacement,
|
||||||
|
buildType: buildValue
|
||||||
|
};
|
||||||
|
switches.push(switchInfo);
|
||||||
|
|
||||||
|
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||||
|
console.log(` File: ${args.importer}`);
|
||||||
|
console.log(` Original: ${args.path}`);
|
||||||
|
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Rewrite the import path and let the normal resolution continue
|
||||||
|
return build.resolve(replacement, {
|
||||||
|
importer: args.importer,
|
||||||
|
namespace: args.namespace,
|
||||||
|
resolveDir: args.resolveDir,
|
||||||
|
kind: args.kind
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
build.onEnd((result) => {
|
||||||
|
if (switches.length > 0) {
|
||||||
|
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
||||||
|
switches.forEach((s, i) => {
|
||||||
|
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
||||||
|
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
esbuild
|
esbuild
|
||||||
.build({
|
.build({
|
||||||
entryPoints: [argv.entry],
|
entryPoints: [argv.entry],
|
||||||
@@ -59,6 +240,9 @@ esbuild
|
|||||||
platform: "node",
|
platform: "node",
|
||||||
external: ["body-parser"],
|
external: ["body-parser"],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
privateImportGuardPlugin(),
|
||||||
|
dynamicImportGuardPlugin(),
|
||||||
|
dynamicImportSwitcherPlugin(argv.build),
|
||||||
nodeExternalsPlugin({
|
nodeExternalsPlugin({
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
@@ -66,7 +250,27 @@ esbuild
|
|||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then((result) => {
|
||||||
|
// Check if there were any errors in the build result
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||||
|
result.errors.forEach((error, i) => {
|
||||||
|
console.error(`${i + 1}. ${error.text}`);
|
||||||
|
if (error.notes) {
|
||||||
|
error.notes.forEach(note => {
|
||||||
|
console.error(` - ${note.text}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove the output file if it was created
|
||||||
|
if (fs.existsSync(argv.out)) {
|
||||||
|
fs.unlinkSync(argv.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Build completed successfully");
|
console.log("Build completed successfully");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
# https://docs.digpangolin.com/
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
{{if .HybridMode}}
|
|
||||||
managed:
|
|
||||||
id: "{{.HybridId}}"
|
|
||||||
secret: "{{.HybridSecret}}"
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
@@ -28,6 +23,7 @@ server:
|
|||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
@@ -40,5 +36,4 @@ flags:
|
|||||||
require_email_verification: {{.EnableEmail}}
|
require_email_verification: {{.EnableEmail}}
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
{{end}}
|
|
||||||
@@ -6,8 +6,6 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- pangolin-data:/var/certificates
|
|
||||||
- pangolin-data:/var/dynamic
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "10s"
|
interval: "10s"
|
||||||
@@ -22,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
@@ -33,7 +31,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
@@ -56,15 +54,9 @@ services:
|
|||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
# Shared volume for certificates and dynamic config in file mode
|
|
||||||
- pangolin-data:/var/certificates:ro
|
|
||||||
- pangolin-data:/var/dynamic:ro
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|
||||||
volumes:
|
|
||||||
pangolin-data:
|
|
||||||
@@ -3,17 +3,12 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
{{if not .HybridMode}}
|
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
{{else}}
|
|
||||||
file:
|
|
||||||
directory: "/var/dynamic"
|
|
||||||
watch: true
|
|
||||||
{{end}}
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
@@ -27,7 +22,7 @@ log:
|
|||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
{{if not .HybridMode}}
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
acme:
|
acme:
|
||||||
@@ -36,22 +31,18 @@ certificatesResolvers:
|
|||||||
email: "{{.LetsEncryptEmail}}"
|
email: "{{.LetsEncryptEmail}}"
|
||||||
storage: "/letsencrypt/acme.json"
|
storage: "/letsencrypt/acme.json"
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
{{end}}
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
{{if .HybridMode}} proxyProtocol:
|
|
||||||
trustedIPs:
|
|
||||||
- 0.0.0.0/0
|
|
||||||
- ::1/128{{end}}
|
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
{{if not .HybridMode}} http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"{{end}}
|
certResolver: "letsencrypt"
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
180
install/get-installer.sh
Normal file
180
install/get-installer.sh
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get installer - Cross-platform installation script
|
||||||
|
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# GitHub repository info
|
||||||
|
REPO="fosrl/pangolin"
|
||||||
|
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get latest version from GitHub API
|
||||||
|
get_latest_version() {
|
||||||
|
local latest_info
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
|
||||||
|
else
|
||||||
|
print_error "Neither curl nor wget is available. Please install one of them." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$latest_info" ]; then
|
||||||
|
print_error "Failed to fetch latest version information" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version from JSON response (works without jq)
|
||||||
|
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
print_error "Could not parse version from GitHub API response" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove 'v' prefix if present
|
||||||
|
version=$(echo "$version" | sed 's/^v//')
|
||||||
|
|
||||||
|
echo "$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect OS and architecture
|
||||||
|
detect_platform() {
|
||||||
|
local os arch
|
||||||
|
|
||||||
|
# Detect OS - only support Linux
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) os="linux" ;;
|
||||||
|
*)
|
||||||
|
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Detect architecture - only support amd64 and arm64
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) arch="amd64" ;;
|
||||||
|
arm64|aarch64) arch="arm64" ;;
|
||||||
|
*)
|
||||||
|
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "${os}_${arch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get installation directory
|
||||||
|
get_install_dir() {
|
||||||
|
# Install to the current directory
|
||||||
|
local install_dir="$(pwd)"
|
||||||
|
if [ ! -d "$install_dir" ]; then
|
||||||
|
print_error "Installation directory does not exist: $install_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$install_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download and install installer
|
||||||
|
install_installer() {
|
||||||
|
local platform="$1"
|
||||||
|
local install_dir="$2"
|
||||||
|
local binary_name="installer_${platform}"
|
||||||
|
|
||||||
|
local download_url="${BASE_URL}/${binary_name}"
|
||||||
|
local temp_file="/tmp/installer"
|
||||||
|
local final_path="${install_dir}/installer"
|
||||||
|
|
||||||
|
print_status "Downloading installer from ${download_url}"
|
||||||
|
|
||||||
|
# Download the binary
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$download_url" -o "$temp_file"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q "$download_url" -O "$temp_file"
|
||||||
|
else
|
||||||
|
print_error "Neither curl nor wget is available. Please install one of them."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create install directory if it doesn't exist
|
||||||
|
mkdir -p "$install_dir"
|
||||||
|
|
||||||
|
# Move binary to install directory
|
||||||
|
mv "$temp_file" "$final_path"
|
||||||
|
|
||||||
|
# Make executable
|
||||||
|
chmod +x "$final_path"
|
||||||
|
|
||||||
|
print_status "Installer downloaded to ${final_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
verify_installation() {
|
||||||
|
local install_dir="$1"
|
||||||
|
local installer_path="${install_dir}/installer"
|
||||||
|
|
||||||
|
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
|
||||||
|
print_status "Installation successful!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Installation failed. Binary not found or not executable."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation process
|
||||||
|
main() {
|
||||||
|
print_status "Installing latest version of installer..."
|
||||||
|
|
||||||
|
# Get latest version
|
||||||
|
print_status "Fetching latest version from GitHub..."
|
||||||
|
VERSION=$(get_latest_version)
|
||||||
|
print_status "Latest version: v${VERSION}"
|
||||||
|
|
||||||
|
# Set base URL with the fetched version
|
||||||
|
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
PLATFORM=$(detect_platform)
|
||||||
|
print_status "Detected platform: ${PLATFORM}"
|
||||||
|
|
||||||
|
# Get install directory
|
||||||
|
INSTALL_DIR=$(get_install_dir)
|
||||||
|
print_status "Install directory: ${INSTALL_DIR}"
|
||||||
|
|
||||||
|
# Install installer
|
||||||
|
install_installer "$PLATFORM" "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
if verify_installation "$INSTALL_DIR"; then
|
||||||
|
print_status "Installer is ready to use!"
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.35.0
|
golang.org/x/term v0.36.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.36.0 // indirect
|
require golang.org/x/sys v0.37.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
195
install/main.go
195
install/main.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -48,10 +47,8 @@ type Config struct {
|
|||||||
InstallGerbil bool
|
InstallGerbil bool
|
||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
HybridMode bool
|
|
||||||
HybridId string
|
|
||||||
HybridSecret string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
@@ -98,24 +95,6 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\n=== Generating Configuration Files ===")
|
fmt.Println("\n=== Generating Configuration Files ===")
|
||||||
|
|
||||||
// If the secret and id are not generated then generate them
|
|
||||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
|
||||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
|
||||||
credentials, err := requestHybridCredentials()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
|
||||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
config.HybridId = credentials.RemoteExitNodeId
|
|
||||||
config.HybridSecret = credentials.Secret
|
|
||||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
|
||||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
|
||||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
|
||||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
|
||||||
readBool(reader, "Have you adopted your node?", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -125,6 +104,15 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
|
// Download MaxMind database if requested
|
||||||
|
if config.EnableGeoblocking {
|
||||||
|
fmt.Println("\n=== Downloading MaxMind Database ===")
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can download it manually later if needed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
@@ -172,9 +160,34 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
alreadyInstalled = true
|
alreadyInstalled = true
|
||||||
fmt.Println("Looks like you already installed Pangolin!")
|
fmt.Println("Looks like you already installed Pangolin!")
|
||||||
|
|
||||||
|
// Check if MaxMind database exists and offer to update it
|
||||||
|
fmt.Println("\n=== MaxMind Database Update ===")
|
||||||
|
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||||
|
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can try updating it manually later if needed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||||
|
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
|
}
|
||||||
|
// Now you need to update your config file accordingly to enable geoblocking
|
||||||
|
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
||||||
|
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||||
|
fmt.Println("Add the following line under the 'server' section:")
|
||||||
|
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||||
@@ -230,7 +243,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.HybridMode && !alreadyInstalled {
|
if !alreadyInstalled {
|
||||||
// Setup Token Section
|
// Setup Token Section
|
||||||
fmt.Println("\n=== Setup Token ===")
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
@@ -251,9 +264,7 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nInstallation complete!")
|
fmt.Println("\nInstallation complete!")
|
||||||
|
|
||||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||||
@@ -328,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
for {
|
|
||||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
|
||||||
config.HybridMode = true
|
// Set default dashboard domain after base domain is collected
|
||||||
break
|
defaultDashboardDomain := ""
|
||||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
if config.BaseDomain != "" {
|
||||||
config.HybridMode = false
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
break
|
}
|
||||||
}
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
fmt.Println("Please answer 'yes' or 'no'")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
|
// Email configuration
|
||||||
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
|
if config.EnableEmail {
|
||||||
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HybridMode {
|
// Validate required fields
|
||||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
if config.BaseDomain == "" {
|
||||||
|
fmt.Println("Error: Domain name is required")
|
||||||
if alreadyHaveCreds {
|
os.Exit(1)
|
||||||
config.HybridId = readString(reader, "Enter your ID", "")
|
}
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
if config.LetsEncryptEmail == "" {
|
||||||
}
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
|
os.Exit(1)
|
||||||
// Try to get public IP as default
|
|
||||||
publicIP := getPublicIP()
|
|
||||||
if publicIP != "" {
|
|
||||||
fmt.Printf("Detected public IP: %s\n", publicIP)
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
|
|
||||||
config.InstallGerbil = true
|
|
||||||
} else {
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
|
||||||
defaultDashboardDomain := ""
|
|
||||||
if config.BaseDomain != "" {
|
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
|
||||||
|
|
||||||
// Email configuration
|
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
|
||||||
|
|
||||||
if config.EnableEmail {
|
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if config.BaseDomain == "" {
|
|
||||||
fmt.Println("Error: Domain name is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if config.LetsEncryptEmail == "" {
|
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced configuration
|
// Advanced configuration
|
||||||
@@ -395,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
fmt.Println("\n=== Advanced Configuration ===")
|
fmt.Println("\n=== Advanced Configuration ===")
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
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 == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
@@ -429,11 +413,6 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// the hybrid does not need the dynamic config
|
|
||||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(path, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
@@ -663,18 +642,30 @@ func checkPortsAvailable(port int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsPangolinInstalledWithHybrid() bool {
|
func downloadMaxMindDatabase() error {
|
||||||
// Check if config/config.yml exists and contains hybrid section
|
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
||||||
return false
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file to check for hybrid section
|
// Extract the database
|
||||||
content, err := os.ReadFile("config/config.yml")
|
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for hybrid section
|
// Find the .mmdb file and move it to the config directory
|
||||||
return bytes.Contains(content, []byte("managed:"))
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
|
|
||||||
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HybridCredentials represents the response from the cloud API
|
|
||||||
type HybridCredentials struct {
|
|
||||||
RemoteExitNodeId string `json:"remoteExitNodeId"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIResponse represents the full response structure from the cloud API
|
|
||||||
type APIResponse struct {
|
|
||||||
Data HybridCredentials `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestPayload represents the request body structure
|
|
||||||
type RequestPayload struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateValidationToken() string {
|
|
||||||
timestamp := time.Now().UnixMilli()
|
|
||||||
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
|
|
||||||
obfuscated := make([]byte, len(data))
|
|
||||||
for i, char := range []byte(data) {
|
|
||||||
obfuscated[i] = char + 5
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(obfuscated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestHybridCredentials makes an HTTP POST request to the cloud API
|
|
||||||
// to get hybrid credentials (ID and secret)
|
|
||||||
func requestHybridCredentials() (*HybridCredentials, error) {
|
|
||||||
// Generate validation token
|
|
||||||
token := generateValidationToken()
|
|
||||||
|
|
||||||
// Create request payload
|
|
||||||
payload := RequestPayload{
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal payload to JSON
|
|
||||||
jsonData, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create HTTP request
|
|
||||||
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Check response status
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read response body for debugging
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the raw JSON response for debugging
|
|
||||||
// fmt.Printf("Raw JSON response: %s\n", string(body))
|
|
||||||
|
|
||||||
// Parse response
|
|
||||||
var apiResponse APIResponse
|
|
||||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode API response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response data
|
|
||||||
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
|
|
||||||
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiResponse.Data, nil
|
|
||||||
}
|
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
|
"siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
|
"siteLocalDescription": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteLocalDescriptionSaas": "Само локални ресурси. Без тунелиране. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Вижте всички сайтове",
|
"siteSeeAll": "Вижте всички сайтове",
|
||||||
"siteTunnelDescription": "Определете как искате да се свържете с вашия сайт",
|
"siteTunnelDescription": "Определете как искате да се свържете с вашия сайт",
|
||||||
"siteNewtCredentials": "Newt Удостоверения",
|
"siteNewtCredentials": "Newt Удостоверения",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Създаден на",
|
"createdAt": "Създаден на",
|
||||||
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
|
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
|
||||||
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
|
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
|
||||||
"proxyEnableSSL": "Активиране на SSL (https)",
|
"proxyEnableSSL": "Активиране на SSL",
|
||||||
|
"proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.",
|
||||||
|
"target": "Цел",
|
||||||
|
"configureTarget": "Конфигуриране на цели",
|
||||||
"targetErrorFetch": "Неуспешно извличане на цели",
|
"targetErrorFetch": "Неуспешно извличане на цели",
|
||||||
"targetErrorFetchDescription": "Възникна грешка при извличане на целите",
|
"targetErrorFetchDescription": "Възникна грешка при извличане на целите",
|
||||||
"siteErrorFetch": "Неуспешно извличане на ресурс",
|
"siteErrorFetch": "Неуспешно извличане на ресурс",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Конфигурация на защитена връзка",
|
"targetTlsSettings": "Конфигурация на защитена връзка",
|
||||||
"targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс",
|
"targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс",
|
||||||
"targetTlsSettingsAdvanced": "Разширени TLS настройки",
|
"targetTlsSettingsAdvanced": "Разширени TLS настройки",
|
||||||
"targetTlsSni": "Име на TLS сървър (SNI)",
|
"targetTlsSni": "Имя на TLS сървър",
|
||||||
"targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.",
|
"targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.",
|
||||||
"targetTlsSubmit": "Запазване на настройките",
|
"targetTlsSubmit": "Запазване на настройките",
|
||||||
"targets": "Конфигурация на целите",
|
"targets": "Конфигурация на целите",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.",
|
"targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.",
|
||||||
"methodSelect": "Изберете метод",
|
"methodSelect": "Изберете метод",
|
||||||
"targetSubmit": "Добавяне на цел",
|
"targetSubmit": "Добавяне на цел",
|
||||||
"targetNoOne": "Няма цели. Добавете цел чрез формата.",
|
"targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.",
|
||||||
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
||||||
"targetsSubmit": "Запазване на целите",
|
"targetsSubmit": "Запазване на целите",
|
||||||
|
"addTarget": "Добавете цел",
|
||||||
|
"targetErrorInvalidIp": "Невалиден IP адрес",
|
||||||
|
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
||||||
|
"targetErrorInvalidPort": "Невалиден порт",
|
||||||
|
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
|
||||||
|
"targetErrorNoSite": "Няма избран сайт",
|
||||||
|
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
|
||||||
|
"targetCreated": "Целта е създадена",
|
||||||
|
"targetCreatedDescription": "Целта беше успешно създадена",
|
||||||
|
"targetErrorCreate": "Неуспешно създаване на целта",
|
||||||
|
"targetErrorCreateDescription": "Възникна грешка при създаването на целта",
|
||||||
|
"save": "Запази",
|
||||||
"proxyAdditional": "Допълнителни настройки на прокси",
|
"proxyAdditional": "Допълнителни настройки на прокси",
|
||||||
"proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки",
|
"proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки",
|
||||||
"proxyCustomHeader": "Персонализиран хост заглавие",
|
"proxyCustomHeader": "Персонализиран хост заглавие",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Администратор на сървър - Панголин",
|
"pangolinServerAdmin": "Администратор на сървър - Панголин",
|
||||||
"licenseTierProfessional": "Професионален лиценз",
|
"licenseTierProfessional": "Професионален лиценз",
|
||||||
"licenseTierEnterprise": "Предприятие лиценз",
|
"licenseTierEnterprise": "Предприятие лиценз",
|
||||||
"licenseTierCommercial": "Търговски лиценз",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Лицензиран",
|
"licensed": "Лицензиран",
|
||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Не",
|
"no": "Не",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Име за показване за този доставчик на идентичност",
|
"idpDisplayName": "Име за показване за този доставчик на идентичност",
|
||||||
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
|
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
|
||||||
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
|
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
|
||||||
"licenseBadge": "Професионален",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Тип доставчик",
|
"idpType": "Тип доставчик",
|
||||||
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
|
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
|
||||||
"idpOidcConfigure": "Конфигурация на OAuth2/OIDC",
|
"idpOidcConfigure": "Конфигурация на OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Навигационно меню",
|
"navbar": "Навигационно меню",
|
||||||
"navbarDescription": "Главно навигационно меню за приложението",
|
"navbarDescription": "Главно навигационно меню за приложението",
|
||||||
"navbarDocsLink": "Документация",
|
"navbarDocsLink": "Документация",
|
||||||
"commercialEdition": "Търговско издание",
|
|
||||||
"otpErrorEnable": "Не може да се активира 2FA",
|
"otpErrorEnable": "Не може да се активира 2FA",
|
||||||
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
|
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
|
||||||
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
|
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Всички потребители",
|
"sidebarAllUsers": "Всички потребители",
|
||||||
"sidebarIdentityProviders": "Идентификационни доставчици",
|
"sidebarIdentityProviders": "Идентификационни доставчици",
|
||||||
"sidebarLicense": "Лиценз",
|
"sidebarLicense": "Лиценз",
|
||||||
"sidebarClients": "Клиенти (Бета)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Домейни",
|
"sidebarDomains": "Домейни",
|
||||||
"enableDockerSocket": "Активиране на Docker Чернова",
|
"enableDockerSocket": "Активиране на Docker Чернова",
|
||||||
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
|
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.",
|
"twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.",
|
||||||
"twoFactor": "Двуфакторно удостоверяване",
|
"twoFactor": "Двуфакторно удостоверяване",
|
||||||
"adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.",
|
"adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.",
|
||||||
"continueToApplication": "Продължете към приложението",
|
|
||||||
"securityKeyAdd": "Добавяне на ключ за сигурност",
|
"securityKeyAdd": "Добавяне на ключ за сигурност",
|
||||||
"securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност",
|
"securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност",
|
||||||
"securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате",
|
"securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Външен прокси разрешен",
|
"externalProxyEnabled": "Външен прокси разрешен",
|
||||||
"addNewTarget": "Добави нова цел",
|
"addNewTarget": "Добави нова цел",
|
||||||
"targetsList": "Списък с цели",
|
"targetsList": "Списък с цели",
|
||||||
|
"advancedMode": "Разширен режим",
|
||||||
"targetErrorDuplicateTargetFound": "Дублирана цел намерена",
|
"targetErrorDuplicateTargetFound": "Дублирана цел намерена",
|
||||||
"healthCheckHealthy": "Здрав",
|
"healthCheckHealthy": "Здрав",
|
||||||
"healthCheckUnhealthy": "Нездрав",
|
"healthCheckUnhealthy": "Нездрав",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Грешка при автоматично влизане",
|
"autoLoginError": "Грешка при автоматично влизане",
|
||||||
"autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.",
|
"autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.",
|
||||||
"autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.",
|
"autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Управление на самостоятелно хоствани",
|
"remoteExitNodeManageRemoteExitNodes": "Отдалечени възли",
|
||||||
"remoteExitNodeDescription": "Управление на възли за разширяване на мрежовата ви свързаност",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Възли",
|
"remoteExitNodes": "Възли",
|
||||||
"searchRemoteExitNodes": "Търсене на възли...",
|
"searchRemoteExitNodes": "Търсене на възли...",
|
||||||
"remoteExitNodeAdd": "Добавяне на възел",
|
"remoteExitNodeAdd": "Добавяне на възел",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.",
|
"remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.",
|
||||||
"remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)",
|
"remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)",
|
||||||
"remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)",
|
"remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)",
|
||||||
"sidebarRemoteExitNodes": "Възли (\"Local\" match)",
|
"sidebarRemoteExitNodes": "Отдалечени възли",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Създаване на възел",
|
"title": "Създаване на възел",
|
||||||
"description": "Създайте нов възел, за да разширите мрежовата си свързаност",
|
"description": "Създайте нов възел, за да разширите мрежовата си свързаност",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Страницата за удостоверяване е актуализирана успешно",
|
"authPageUpdated": "Страницата за удостоверяване е актуализирана успешно",
|
||||||
"healthCheckNotAvailable": "Локална",
|
"healthCheckNotAvailable": "Локална",
|
||||||
"rewritePath": "Пренапиши път",
|
"rewritePath": "Пренапиши път",
|
||||||
"rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.",
|
||||||
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Zobrazit všechny lokality",
|
"siteSeeAll": "Zobrazit všechny lokality",
|
||||||
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
||||||
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Vytvořeno v",
|
"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.",
|
"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.",
|
"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 (https)",
|
"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",
|
"targetErrorFetch": "Nepodařilo se načíst cíle",
|
||||||
"targetErrorFetchDescription": "Při načítání cílů došlo k chybě",
|
"targetErrorFetchDescription": "Při načítání cílů došlo k chybě",
|
||||||
"siteErrorFetch": "Nepodařilo se načíst zdroj",
|
"siteErrorFetch": "Nepodařilo se načíst zdroj",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Nastavení bezpečného připojení",
|
"targetTlsSettings": "Nastavení bezpečného připojení",
|
||||||
"targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument",
|
"targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument",
|
||||||
"targetTlsSettingsAdvanced": "Pokročilé nastavení TLS",
|
"targetTlsSettingsAdvanced": "Pokročilé nastavení TLS",
|
||||||
"targetTlsSni": "Název serveru TLS (SNI)",
|
"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í.",
|
"targetTlsSniDescription": "Název serveru TLS pro použití v SNI. Ponechte prázdné pro použití výchozího nastavení.",
|
||||||
"targetTlsSubmit": "Uložit nastavení",
|
"targetTlsSubmit": "Uložit nastavení",
|
||||||
"targets": "Konfigurace cílů",
|
"targets": "Konfigurace cílů",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
|
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
|
||||||
"methodSelect": "Vyberte metodu",
|
"methodSelect": "Vyberte metodu",
|
||||||
"targetSubmit": "Add Target",
|
"targetSubmit": "Add Target",
|
||||||
"targetNoOne": "Žádné cíle. Přidejte cíl pomocí formuláře.",
|
"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í.",
|
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
||||||
"targetsSubmit": "Uložit cíle",
|
"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",
|
"proxyAdditional": "Další nastavení proxy",
|
||||||
"proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje",
|
"proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje",
|
||||||
"proxyCustomHeader": "Vlastní hlavička hostitele",
|
"proxyCustomHeader": "Vlastní hlavička hostitele",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Správce serveru - Pangolin",
|
"pangolinServerAdmin": "Správce serveru - Pangolin",
|
||||||
"licenseTierProfessional": "Profesionální licence",
|
"licenseTierProfessional": "Profesionální licence",
|
||||||
"licenseTierEnterprise": "Podniková licence",
|
"licenseTierEnterprise": "Podniková licence",
|
||||||
"licenseTierCommercial": "Obchodní licence",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Licencováno",
|
"licensed": "Licencováno",
|
||||||
"yes": "Ano",
|
"yes": "Ano",
|
||||||
"no": "Ne",
|
"no": "Ne",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
|
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
|
||||||
"idpAutoProvisionUsers": "Automatická úprava uživatelů",
|
"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.",
|
"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": "Profesionální",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Typ poskytovatele",
|
"idpType": "Typ poskytovatele",
|
||||||
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
|
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
|
||||||
"idpOidcConfigure": "Nastavení OAuth2/OIDC",
|
"idpOidcConfigure": "Nastavení OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Hlavní navigační menu aplikace",
|
"navbarDescription": "Hlavní navigační menu aplikace",
|
||||||
"navbarDocsLink": "Dokumentace",
|
"navbarDocsLink": "Dokumentace",
|
||||||
"commercialEdition": "Obchodní vydání",
|
|
||||||
"otpErrorEnable": "2FA nelze povolit",
|
"otpErrorEnable": "2FA nelze povolit",
|
||||||
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
|
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
|
||||||
"otpSetupCheckCode": "Zadejte 6místný kód",
|
"otpSetupCheckCode": "Zadejte 6místný kód",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Všichni uživatelé",
|
"sidebarAllUsers": "Všichni uživatelé",
|
||||||
"sidebarIdentityProviders": "Poskytovatelé identity",
|
"sidebarIdentityProviders": "Poskytovatelé identity",
|
||||||
"sidebarLicense": "Licence",
|
"sidebarLicense": "Licence",
|
||||||
"sidebarClients": "Klienti (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domény",
|
"sidebarDomains": "Domény",
|
||||||
"enableDockerSocket": "Povolit Docker plán",
|
"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.",
|
"enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.",
|
"twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.",
|
||||||
"twoFactor": "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í.",
|
"adminEnabled2FaOnYourAccount": "Váš správce povolil dvoufaktorové ověřování pro {email}. Chcete-li pokračovat, dokončete proces nastavení.",
|
||||||
"continueToApplication": "Pokračovat v aplikaci",
|
|
||||||
"securityKeyAdd": "Přidat bezpečnostní klíč",
|
"securityKeyAdd": "Přidat bezpečnostní klíč",
|
||||||
"securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč",
|
"securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč",
|
||||||
"securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci",
|
"securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Externí proxy povolen",
|
"externalProxyEnabled": "Externí proxy povolen",
|
||||||
"addNewTarget": "Add New Target",
|
"addNewTarget": "Add New Target",
|
||||||
"targetsList": "Seznam cílů",
|
"targetsList": "Seznam cílů",
|
||||||
|
"advancedMode": "Pokročilý režim",
|
||||||
"targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl",
|
"targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl",
|
||||||
"healthCheckHealthy": "Zdravé",
|
"healthCheckHealthy": "Zdravé",
|
||||||
"healthCheckUnhealthy": "Nezdravé",
|
"healthCheckUnhealthy": "Nezdravé",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Automatická chyba přihlášení",
|
"autoLoginError": "Automatická chyba přihlášení",
|
||||||
"autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.",
|
"autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.",
|
||||||
"autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.",
|
"autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Spravovat vlastní hostování",
|
"remoteExitNodeManageRemoteExitNodes": "Vzdálené uzly",
|
||||||
"remoteExitNodeDescription": "Spravujte uzly pro rozšíření připojení k síti",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Uzly",
|
"remoteExitNodes": "Uzly",
|
||||||
"searchRemoteExitNodes": "Hledat uzly...",
|
"searchRemoteExitNodes": "Hledat uzly...",
|
||||||
"remoteExitNodeAdd": "Přidat uzel",
|
"remoteExitNodeAdd": "Přidat uzel",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.",
|
"remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.",
|
||||||
"remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu",
|
"remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu",
|
||||||
"remoteExitNodeDelete": "Odstranit uzel",
|
"remoteExitNodeDelete": "Odstranit uzel",
|
||||||
"sidebarRemoteExitNodes": "Uzly",
|
"sidebarRemoteExitNodes": "Vzdálené uzly",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Vytvořit uzel",
|
"title": "Vytvořit uzel",
|
||||||
"description": "Vytvořit nový uzel pro rozšíření síťového připojení",
|
"description": "Vytvořit nový uzel pro rozšíření síťového připojení",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Autentizační stránka byla úspěšně aktualizována",
|
"authPageUpdated": "Autentizační stránka byla úspěšně aktualizována",
|
||||||
"healthCheckNotAvailable": "Místní",
|
"healthCheckNotAvailable": "Místní",
|
||||||
"rewritePath": "Přepsat cestu",
|
"rewritePath": "Přepsat cestu",
|
||||||
"rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Alle Standorte anzeigen",
|
"siteSeeAll": "Alle Standorte anzeigen",
|
||||||
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
||||||
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Erstellt am",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.",
|
||||||
"proxyEnableSSL": "SSL aktivieren (https)",
|
"proxyEnableSSL": "SSL aktivieren",
|
||||||
|
"proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Ziele konfigurieren",
|
||||||
"targetErrorFetch": "Fehler beim Abrufen der Ziele",
|
"targetErrorFetch": "Fehler beim Abrufen der Ziele",
|
||||||
"targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten",
|
"targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten",
|
||||||
"siteErrorFetch": "Fehler beim Abrufen der Ressource",
|
"siteErrorFetch": "Fehler beim Abrufen der Ressource",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Sicherheitskonfiguration",
|
"targetTlsSettings": "Sicherheitskonfiguration",
|
||||||
"targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource",
|
"targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource",
|
||||||
"targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen",
|
"targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen",
|
||||||
"targetTlsSni": "TLS-Servername (SNI)",
|
"targetTlsSni": "TLS Servername",
|
||||||
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
||||||
"targetTlsSubmit": "Einstellungen speichern",
|
"targetTlsSubmit": "Einstellungen speichern",
|
||||||
"targets": "Ziel-Konfiguration",
|
"targets": "Ziel-Konfiguration",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
||||||
"methodSelect": "Methode auswählen",
|
"methodSelect": "Methode auswählen",
|
||||||
"targetSubmit": "Ziel hinzufügen",
|
"targetSubmit": "Ziel hinzufügen",
|
||||||
"targetNoOne": "Keine Ziele. Fügen Sie ein Ziel über das Formular hinzu.",
|
"targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.",
|
||||||
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
||||||
"targetsSubmit": "Ziele speichern",
|
"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",
|
"proxyAdditional": "Zusätzliche Proxy-Einstellungen",
|
||||||
"proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht",
|
"proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht",
|
||||||
"proxyCustomHeader": "Benutzerdefinierter Host-Header",
|
"proxyCustomHeader": "Benutzerdefinierter Host-Header",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||||
"licenseTierProfessional": "Professional Lizenz",
|
"licenseTierProfessional": "Professional Lizenz",
|
||||||
"licenseTierEnterprise": "Enterprise Lizenz",
|
"licenseTierEnterprise": "Enterprise Lizenz",
|
||||||
"licenseTierCommercial": "Gewerbliche Lizenz",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Lizenziert",
|
"licensed": "Lizenziert",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
|
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
|
||||||
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
|
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
|
||||||
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
|
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
|
||||||
"licenseBadge": "Profi",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Anbietertyp",
|
"idpType": "Anbietertyp",
|
||||||
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
|
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC Konfiguration",
|
"idpOidcConfigure": "OAuth2/OIDC Konfiguration",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Navigationsmenü",
|
"navbar": "Navigationsmenü",
|
||||||
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
|
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
|
||||||
"navbarDocsLink": "Dokumentation",
|
"navbarDocsLink": "Dokumentation",
|
||||||
"commercialEdition": "Kommerzielle Edition",
|
|
||||||
"otpErrorEnable": "2FA konnte nicht aktiviert werden",
|
"otpErrorEnable": "2FA konnte nicht aktiviert werden",
|
||||||
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
|
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
|
||||||
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
|
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Alle Benutzer",
|
"sidebarAllUsers": "Alle Benutzer",
|
||||||
"sidebarIdentityProviders": "Identitätsanbieter",
|
"sidebarIdentityProviders": "Identitätsanbieter",
|
||||||
"sidebarLicense": "Lizenz",
|
"sidebarLicense": "Lizenz",
|
||||||
"sidebarClients": "Kunden (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domänen",
|
"sidebarDomains": "Domänen",
|
||||||
"enableDockerSocket": "Docker Blaupause aktivieren",
|
"enableDockerSocket": "Docker Blaupause aktivieren",
|
||||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.",
|
"twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.",
|
||||||
"twoFactor": "Zwei-Faktor-Authentifizierung",
|
"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.",
|
"adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.",
|
||||||
"continueToApplication": "Weiter zur Anwendung",
|
|
||||||
"securityKeyAdd": "Sicherheitsschlüssel hinzufügen",
|
"securityKeyAdd": "Sicherheitsschlüssel hinzufügen",
|
||||||
"securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren",
|
"securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren",
|
||||||
"securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren",
|
"securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Externer Proxy aktiviert",
|
"externalProxyEnabled": "Externer Proxy aktiviert",
|
||||||
"addNewTarget": "Neues Ziel hinzufügen",
|
"addNewTarget": "Neues Ziel hinzufügen",
|
||||||
"targetsList": "Ziel-Liste",
|
"targetsList": "Ziel-Liste",
|
||||||
|
"advancedMode": "Erweiterter Modus",
|
||||||
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
|
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
|
||||||
"healthCheckHealthy": "Gesund",
|
"healthCheckHealthy": "Gesund",
|
||||||
"healthCheckUnhealthy": "Ungesund",
|
"healthCheckUnhealthy": "Ungesund",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
||||||
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
||||||
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
|
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Selbst-Hosted verwalten",
|
"remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten",
|
||||||
"remoteExitNodeDescription": "Knoten verwalten, um die Netzwerkverbindung zu erweitern",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Knoten",
|
"remoteExitNodes": "Knoten",
|
||||||
"searchRemoteExitNodes": "Knoten suchen...",
|
"searchRemoteExitNodes": "Knoten suchen...",
|
||||||
"remoteExitNodeAdd": "Knoten hinzufügen",
|
"remoteExitNodeAdd": "Knoten hinzufügen",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.",
|
"remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.",
|
||||||
"remoteExitNodeConfirmDelete": "Löschknoten bestätigen",
|
"remoteExitNodeConfirmDelete": "Löschknoten bestätigen",
|
||||||
"remoteExitNodeDelete": "Knoten löschen",
|
"remoteExitNodeDelete": "Knoten löschen",
|
||||||
"sidebarRemoteExitNodes": "Knoten",
|
"sidebarRemoteExitNodes": "Entfernte Knoten",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Knoten erstellen",
|
"title": "Knoten erstellen",
|
||||||
"description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern",
|
"description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Auth-Seite erfolgreich aktualisiert",
|
"authPageUpdated": "Auth-Seite erfolgreich aktualisiert",
|
||||||
"healthCheckNotAvailable": "Lokal",
|
"healthCheckNotAvailable": "Lokal",
|
||||||
"rewritePath": "Pfad neu schreiben",
|
"rewritePath": "Pfad neu schreiben",
|
||||||
"rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"setupCreate": "Create your organization, site, and resources",
|
"setupCreate": "Create your organization, site, and resources",
|
||||||
"setupNewOrg": "New Organization",
|
"setupNewOrg": "New Organization",
|
||||||
@@ -97,7 +96,7 @@
|
|||||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
"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.",
|
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling.",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "See All Sites",
|
"siteSeeAll": "See All Sites",
|
||||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||||
"siteNewtCredentials": "Newt Credentials",
|
"siteNewtCredentials": "Newt Credentials",
|
||||||
@@ -469,7 +468,10 @@
|
|||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
"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.",
|
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||||
"proxyEnableSSL": "Enable SSL (https)",
|
"proxyEnableSSL": "Enable SSL",
|
||||||
|
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configure Targets",
|
||||||
"targetErrorFetch": "Failed to fetch targets",
|
"targetErrorFetch": "Failed to fetch targets",
|
||||||
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
||||||
"siteErrorFetch": "Failed to fetch resource",
|
"siteErrorFetch": "Failed to fetch resource",
|
||||||
@@ -496,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Secure Connection Configuration",
|
"targetTlsSettings": "Secure Connection Configuration",
|
||||||
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
||||||
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
||||||
"targetTlsSni": "TLS Server Name (SNI)",
|
"targetTlsSni": "TLS Server Name",
|
||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
@@ -505,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||||
"methodSelect": "Select method",
|
"methodSelect": "Select method",
|
||||||
"targetSubmit": "Add Target",
|
"targetSubmit": "Add Target",
|
||||||
"targetNoOne": "No targets. Add a target using the form.",
|
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
|
||||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"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",
|
"proxyAdditional": "Additional Proxy Settings",
|
||||||
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
||||||
"proxyCustomHeader": "Custom Host Header",
|
"proxyCustomHeader": "Custom Host Header",
|
||||||
@@ -716,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||||
"licenseTierProfessional": "Professional License",
|
"licenseTierProfessional": "Professional License",
|
||||||
"licenseTierEnterprise": "Enterprise License",
|
"licenseTierEnterprise": "Enterprise License",
|
||||||
"licenseTierCommercial": "Commercial License",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Licensed",
|
"licensed": "Licensed",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
@@ -751,7 +765,7 @@
|
|||||||
"idpDisplayName": "A display name for this identity provider",
|
"idpDisplayName": "A display name for this identity provider",
|
||||||
"idpAutoProvisionUsers": "Auto Provision Users",
|
"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.",
|
"idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.",
|
||||||
"licenseBadge": "Professional",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Provider Type",
|
"idpType": "Provider Type",
|
||||||
"idpTypeDescription": "Select the type of identity provider you want to configure",
|
"idpTypeDescription": "Select the type of identity provider you want to configure",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
||||||
@@ -1041,26 +1055,6 @@
|
|||||||
"actionDeleteResourceRule": "Delete Resource Rule",
|
"actionDeleteResourceRule": "Delete Resource Rule",
|
||||||
"actionListResourceRules": "List Resource Rules",
|
"actionListResourceRules": "List Resource Rules",
|
||||||
"actionUpdateResourceRule": "Update Resource Rule",
|
"actionUpdateResourceRule": "Update Resource Rule",
|
||||||
"ruleTemplates": "Rule Templates",
|
|
||||||
"ruleTemplatesDescription": "Assign rule templates to automatically apply consistent rules across multiple resources",
|
|
||||||
"ruleTemplatesSearch": "Search templates...",
|
|
||||||
"ruleTemplateAdd": "Create Template",
|
|
||||||
"ruleTemplateErrorDelete": "Failed to delete template",
|
|
||||||
"ruleTemplateCreated": "Template created",
|
|
||||||
"ruleTemplateCreatedDescription": "Rule template created successfully",
|
|
||||||
"ruleTemplateErrorCreate": "Failed to create template",
|
|
||||||
"ruleTemplateErrorCreateDescription": "An error occurred while creating the template",
|
|
||||||
"ruleTemplateSetting": "Rule Template Settings",
|
|
||||||
"ruleTemplateSettingDescription": "Manage template details and rules",
|
|
||||||
"ruleTemplateErrorLoad": "Failed to load template",
|
|
||||||
"ruleTemplateErrorLoadDescription": "An error occurred while loading the template",
|
|
||||||
"ruleTemplateUpdated": "Template updated",
|
|
||||||
"ruleTemplateUpdatedDescription": "Template updated successfully",
|
|
||||||
"ruleTemplateErrorUpdate": "Failed to update template",
|
|
||||||
"ruleTemplateErrorUpdateDescription": "An error occurred while updating the template",
|
|
||||||
"save": "Save",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"templateDetails": "Template Details",
|
|
||||||
"actionListOrgs": "List Organizations",
|
"actionListOrgs": "List Organizations",
|
||||||
"actionCheckOrgId": "Check ID",
|
"actionCheckOrgId": "Check ID",
|
||||||
"actionCreateOrg": "Create Organization",
|
"actionCreateOrg": "Create Organization",
|
||||||
@@ -1105,7 +1099,6 @@
|
|||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Main navigation menu for the application",
|
"navbarDescription": "Main navigation menu for the application",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
"commercialEdition": "Commercial Edition",
|
|
||||||
"otpErrorEnable": "Unable to enable 2FA",
|
"otpErrorEnable": "Unable to enable 2FA",
|
||||||
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
|
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
|
||||||
"otpSetupCheckCode": "Please enter a 6-digit code",
|
"otpSetupCheckCode": "Please enter a 6-digit code",
|
||||||
@@ -1156,13 +1149,12 @@
|
|||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
"sidebarRoles": "Roles",
|
"sidebarRoles": "Roles",
|
||||||
"sidebarShareableLinks": "Shareable Links",
|
"sidebarShareableLinks": "Shareable Links",
|
||||||
"sidebarRuleTemplates": "Rule Templates",
|
|
||||||
"sidebarApiKeys": "API Keys",
|
"sidebarApiKeys": "API Keys",
|
||||||
"sidebarSettings": "Settings",
|
"sidebarSettings": "Settings",
|
||||||
"sidebarAllUsers": "All Users",
|
"sidebarAllUsers": "All Users",
|
||||||
"sidebarIdentityProviders": "Identity Providers",
|
"sidebarIdentityProviders": "Identity Providers",
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||||
@@ -1355,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
||||||
"twoFactor": "Two-Factor Authentication",
|
"twoFactor": "Two-Factor Authentication",
|
||||||
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
||||||
"continueToApplication": "Continue to Application",
|
|
||||||
"securityKeyAdd": "Add Security Key",
|
"securityKeyAdd": "Add Security Key",
|
||||||
"securityKeyRegisterTitle": "Register New Security Key",
|
"securityKeyRegisterTitle": "Register New Security Key",
|
||||||
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
|
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
|
||||||
@@ -1433,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "External Proxy Enabled",
|
"externalProxyEnabled": "External Proxy Enabled",
|
||||||
"addNewTarget": "Add New Target",
|
"addNewTarget": "Add New Target",
|
||||||
"targetsList": "Targets List",
|
"targetsList": "Targets List",
|
||||||
|
"advancedMode": "Advanced Mode",
|
||||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||||
"healthCheckHealthy": "Healthy",
|
"healthCheckHealthy": "Healthy",
|
||||||
"healthCheckUnhealthy": "Unhealthy",
|
"healthCheckUnhealthy": "Unhealthy",
|
||||||
@@ -1565,8 +1557,8 @@
|
|||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted",
|
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeDescription": "Manage nodes to extend your network connectivity",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Search nodes...",
|
"searchRemoteExitNodes": "Search nodes...",
|
||||||
"remoteExitNodeAdd": "Add Node",
|
"remoteExitNodeAdd": "Add Node",
|
||||||
@@ -1576,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||||
"remoteExitNodeDelete": "Delete Node",
|
"remoteExitNodeDelete": "Delete Node",
|
||||||
"sidebarRemoteExitNodes": "Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Node",
|
"title": "Create Node",
|
||||||
"description": "Create a new node to extend your network connectivity",
|
"description": "Create a new node to extend your network connectivity",
|
||||||
@@ -1746,5 +1738,161 @@
|
|||||||
"healthCheckNotAvailable": "Local",
|
"healthCheckNotAvailable": "Local",
|
||||||
"rewritePath": "Rewrite Path",
|
"rewritePath": "Rewrite Path",
|
||||||
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
||||||
"continueToApplication": "Continue to application"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Solo recursos locales. Sin túneles.",
|
||||||
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Ver todos los sitios",
|
"siteSeeAll": "Ver todos los sitios",
|
||||||
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
||||||
"siteNewtCredentials": "Credenciales nuevas",
|
"siteNewtCredentials": "Credenciales nuevas",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Creado el",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
|
||||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
"proxyEnableSSL": "Activar SSL",
|
||||||
|
"proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configurar objetivos",
|
||||||
"targetErrorFetch": "Error al recuperar los objetivos",
|
"targetErrorFetch": "Error al recuperar los objetivos",
|
||||||
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
|
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
|
||||||
"siteErrorFetch": "No se pudo obtener el recurso",
|
"siteErrorFetch": "No se pudo obtener el recurso",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Configuración de conexión segura",
|
"targetTlsSettings": "Configuración de conexión segura",
|
||||||
"targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso",
|
"targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso",
|
||||||
"targetTlsSettingsAdvanced": "Ajustes avanzados de TLS",
|
"targetTlsSettingsAdvanced": "Ajustes avanzados de TLS",
|
||||||
"targetTlsSni": "Nombre del servidor TLS (SNI)",
|
"targetTlsSni": "Nombre del servidor TLS",
|
||||||
"targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.",
|
"targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.",
|
||||||
"targetTlsSubmit": "Guardar ajustes",
|
"targetTlsSubmit": "Guardar ajustes",
|
||||||
"targets": "Configuración de objetivos",
|
"targets": "Configuración de objetivos",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.",
|
"targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.",
|
||||||
"methodSelect": "Seleccionar método",
|
"methodSelect": "Seleccionar método",
|
||||||
"targetSubmit": "Añadir destino",
|
"targetSubmit": "Añadir destino",
|
||||||
"targetNoOne": "No hay objetivos. Agregue un objetivo usando el formulario.",
|
"targetNoOne": "Este recurso no tiene ningún objetivo. Agrega un objetivo para configurar dónde enviar peticiones al backend.",
|
||||||
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
||||||
"targetsSubmit": "Guardar objetivos",
|
"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",
|
"proxyAdditional": "Ajustes adicionales del proxy",
|
||||||
"proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy",
|
"proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy",
|
||||||
"proxyCustomHeader": "Cabecera de host personalizada",
|
"proxyCustomHeader": "Cabecera de host personalizada",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Admin Servidor - Pangolin",
|
"pangolinServerAdmin": "Admin Servidor - Pangolin",
|
||||||
"licenseTierProfessional": "Licencia profesional",
|
"licenseTierProfessional": "Licencia profesional",
|
||||||
"licenseTierEnterprise": "Licencia Enterprise",
|
"licenseTierEnterprise": "Licencia Enterprise",
|
||||||
"licenseTierCommercial": "Licencia comercial",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Licenciado",
|
"licensed": "Licenciado",
|
||||||
"yes": "Sí",
|
"yes": "Sí",
|
||||||
"no": "Nu",
|
"no": "Nu",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
|
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
|
||||||
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.",
|
||||||
"licenseBadge": "Profesional",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Tipo de proveedor",
|
"idpType": "Tipo de proveedor",
|
||||||
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
|
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
|
||||||
"idpOidcConfigure": "Configuración OAuth2/OIDC",
|
"idpOidcConfigure": "Configuración OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Menú de navegación",
|
"navbar": "Menú de navegación",
|
||||||
"navbarDescription": "Menú de navegación principal para la aplicación",
|
"navbarDescription": "Menú de navegación principal para la aplicación",
|
||||||
"navbarDocsLink": "Documentación",
|
"navbarDocsLink": "Documentación",
|
||||||
"commercialEdition": "Edición Comercial",
|
|
||||||
"otpErrorEnable": "No se puede habilitar 2FA",
|
"otpErrorEnable": "No se puede habilitar 2FA",
|
||||||
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
|
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
|
||||||
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
|
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Todos los usuarios",
|
"sidebarAllUsers": "Todos los usuarios",
|
||||||
"sidebarIdentityProviders": "Proveedores de identidad",
|
"sidebarIdentityProviders": "Proveedores de identidad",
|
||||||
"sidebarLicense": "Licencia",
|
"sidebarLicense": "Licencia",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Dominios",
|
"sidebarDomains": "Dominios",
|
||||||
"enableDockerSocket": "Habilitar Plano Docker",
|
"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.",
|
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.",
|
"twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.",
|
||||||
"twoFactor": "Autenticación de dos factores",
|
"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.",
|
"adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.",
|
||||||
"continueToApplication": "Continuar a la aplicación",
|
|
||||||
"securityKeyAdd": "Agregar llave de seguridad",
|
"securityKeyAdd": "Agregar llave de seguridad",
|
||||||
"securityKeyRegisterTitle": "Registrar nueva llave de seguridad",
|
"securityKeyRegisterTitle": "Registrar nueva llave de seguridad",
|
||||||
"securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla",
|
"securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Proxy externo habilitado",
|
"externalProxyEnabled": "Proxy externo habilitado",
|
||||||
"addNewTarget": "Agregar nuevo destino",
|
"addNewTarget": "Agregar nuevo destino",
|
||||||
"targetsList": "Lista de destinos",
|
"targetsList": "Lista de destinos",
|
||||||
|
"advancedMode": "Modo avanzado",
|
||||||
"targetErrorDuplicateTargetFound": "Se encontró un destino duplicado",
|
"targetErrorDuplicateTargetFound": "Se encontró un destino duplicado",
|
||||||
"healthCheckHealthy": "Saludable",
|
"healthCheckHealthy": "Saludable",
|
||||||
"healthCheckUnhealthy": "No saludable",
|
"healthCheckUnhealthy": "No saludable",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Error de inicio de sesión automático",
|
"autoLoginError": "Error de inicio de sesión automático",
|
||||||
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
||||||
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
|
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Administrar Nodos Autogestionados",
|
"remoteExitNodeManageRemoteExitNodes": "Nodos remotos",
|
||||||
"remoteExitNodeDescription": "Administrar nodos para extender la conectividad de red",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nodos",
|
"remoteExitNodes": "Nodos",
|
||||||
"searchRemoteExitNodes": "Buscar nodos...",
|
"searchRemoteExitNodes": "Buscar nodos...",
|
||||||
"remoteExitNodeAdd": "Añadir Nodo",
|
"remoteExitNodeAdd": "Añadir Nodo",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.",
|
"remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirmar eliminar nodo",
|
"remoteExitNodeConfirmDelete": "Confirmar eliminar nodo",
|
||||||
"remoteExitNodeDelete": "Eliminar Nodo",
|
"remoteExitNodeDelete": "Eliminar Nodo",
|
||||||
"sidebarRemoteExitNodes": "Nodos",
|
"sidebarRemoteExitNodes": "Nodos remotos",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Crear Nodo",
|
"title": "Crear Nodo",
|
||||||
"description": "Crear un nuevo nodo para extender la conectividad de red",
|
"description": "Crear un nuevo nodo para extender la conectividad de red",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Página auth actualizada correctamente",
|
"authPageUpdated": "Página auth actualizada correctamente",
|
||||||
"healthCheckNotAvailable": "Local",
|
"healthCheckNotAvailable": "Local",
|
||||||
"rewritePath": "Reescribir Ruta",
|
"rewritePath": "Reescribir Ruta",
|
||||||
"rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Voir tous les sites",
|
"siteSeeAll": "Voir tous les sites",
|
||||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||||
"siteNewtCredentials": "Identifiants Newt",
|
"siteNewtCredentials": "Identifiants Newt",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Créé le",
|
"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é.",
|
"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.",
|
"proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.",
|
||||||
"proxyEnableSSL": "Activer SSL (https)",
|
"proxyEnableSSL": "Activer SSL",
|
||||||
|
"proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configurer les cibles",
|
||||||
"targetErrorFetch": "Échec de la récupération des cibles",
|
"targetErrorFetch": "Échec de la récupération des cibles",
|
||||||
"targetErrorFetchDescription": "Une erreur s'est produite lors 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",
|
"siteErrorFetch": "Échec de la récupération de la ressource",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Configuration sécurisée de connexion",
|
"targetTlsSettings": "Configuration sécurisée de connexion",
|
||||||
"targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource",
|
"targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource",
|
||||||
"targetTlsSettingsAdvanced": "Paramètres TLS avancés",
|
"targetTlsSettingsAdvanced": "Paramètres TLS avancés",
|
||||||
"targetTlsSni": "Nom de serveur TLS (SNI)",
|
"targetTlsSni": "Nom du serveur TLS",
|
||||||
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
||||||
"targetTlsSubmit": "Enregistrer les paramètres",
|
"targetTlsSubmit": "Enregistrer les paramètres",
|
||||||
"targets": "Configuration des cibles",
|
"targets": "Configuration des cibles",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
||||||
"methodSelect": "Sélectionner la méthode",
|
"methodSelect": "Sélectionner la méthode",
|
||||||
"targetSubmit": "Ajouter une cible",
|
"targetSubmit": "Ajouter une cible",
|
||||||
"targetNoOne": "Aucune cible. Ajoutez une cible en utilisant le formulaire.",
|
"targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.",
|
||||||
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
||||||
"targetsSubmit": "Enregistrer les cibles",
|
"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",
|
"proxyAdditional": "Paramètres de proxy supplémentaires",
|
||||||
"proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy",
|
"proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy",
|
||||||
"proxyCustomHeader": "En-tête Host personnalisé",
|
"proxyCustomHeader": "En-tête Host personnalisé",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Admin Serveur - Pangolin",
|
"pangolinServerAdmin": "Admin Serveur - Pangolin",
|
||||||
"licenseTierProfessional": "Licence Professionnelle",
|
"licenseTierProfessional": "Licence Professionnelle",
|
||||||
"licenseTierEnterprise": "Licence Entreprise",
|
"licenseTierEnterprise": "Licence Entreprise",
|
||||||
"licenseTierCommercial": "Licence commerciale",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Sous licence",
|
"licensed": "Sous licence",
|
||||||
"yes": "Oui",
|
"yes": "Oui",
|
||||||
"no": "Non",
|
"no": "Non",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
|
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
|
||||||
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.",
|
||||||
"licenseBadge": "Professionnel",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Type de fournisseur",
|
"idpType": "Type de fournisseur",
|
||||||
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
|
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
|
||||||
"idpOidcConfigure": "Configuration OAuth2/OIDC",
|
"idpOidcConfigure": "Configuration OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Menu de navigation",
|
"navbar": "Menu de navigation",
|
||||||
"navbarDescription": "Menu de navigation principal de l'application",
|
"navbarDescription": "Menu de navigation principal de l'application",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
"commercialEdition": "Édition Commerciale",
|
|
||||||
"otpErrorEnable": "Impossible d'activer l'A2F",
|
"otpErrorEnable": "Impossible d'activer l'A2F",
|
||||||
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
|
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
|
||||||
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
|
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Tous les utilisateurs",
|
"sidebarAllUsers": "Tous les utilisateurs",
|
||||||
"sidebarIdentityProviders": "Fournisseurs d'identité",
|
"sidebarIdentityProviders": "Fournisseurs d'identité",
|
||||||
"sidebarLicense": "Licence",
|
"sidebarLicense": "Licence",
|
||||||
"sidebarClients": "Clients (Bêta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domaines",
|
"sidebarDomains": "Domaines",
|
||||||
"enableDockerSocket": "Activer le Plan Docker",
|
"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.",
|
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.",
|
"twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.",
|
||||||
"twoFactor": "Authentification à deux facteurs",
|
"twoFactor": "Authentification à deux facteurs",
|
||||||
"adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.",
|
"adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.",
|
||||||
"continueToApplication": "Continuer vers l'application",
|
|
||||||
"securityKeyAdd": "Ajouter une clé de sécurité",
|
"securityKeyAdd": "Ajouter une clé de sécurité",
|
||||||
"securityKeyRegisterTitle": "Enregistrer une nouvelle 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",
|
"securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Proxy externe activé",
|
"externalProxyEnabled": "Proxy externe activé",
|
||||||
"addNewTarget": "Ajouter une nouvelle cible",
|
"addNewTarget": "Ajouter une nouvelle cible",
|
||||||
"targetsList": "Liste des cibles",
|
"targetsList": "Liste des cibles",
|
||||||
|
"advancedMode": "Mode Avancé",
|
||||||
"targetErrorDuplicateTargetFound": "Cible en double trouvée",
|
"targetErrorDuplicateTargetFound": "Cible en double trouvée",
|
||||||
"healthCheckHealthy": "Sain",
|
"healthCheckHealthy": "Sain",
|
||||||
"healthCheckUnhealthy": "En mauvaise santé",
|
"healthCheckUnhealthy": "En mauvaise santé",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Erreur de connexion automatique",
|
"autoLoginError": "Erreur de connexion automatique",
|
||||||
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
||||||
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
|
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Gérer auto-hébergé",
|
"remoteExitNodeManageRemoteExitNodes": "Nœuds distants",
|
||||||
"remoteExitNodeDescription": "Gérer les nœuds pour étendre votre connectivité réseau",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nœuds",
|
"remoteExitNodes": "Nœuds",
|
||||||
"searchRemoteExitNodes": "Rechercher des nœuds...",
|
"searchRemoteExitNodes": "Rechercher des nœuds...",
|
||||||
"remoteExitNodeAdd": "Ajouter un noeud",
|
"remoteExitNodeAdd": "Ajouter un noeud",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.",
|
"remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud",
|
"remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud",
|
||||||
"remoteExitNodeDelete": "Supprimer le noeud",
|
"remoteExitNodeDelete": "Supprimer le noeud",
|
||||||
"sidebarRemoteExitNodes": "Nœuds",
|
"sidebarRemoteExitNodes": "Nœuds distants",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Créer un noeud",
|
"title": "Créer un noeud",
|
||||||
"description": "Créer un nouveau nœud pour étendre votre connectivité réseau",
|
"description": "Créer un nouveau nœud pour étendre votre connectivité réseau",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Page d\u000027authentification mise à jour avec succès",
|
"authPageUpdated": "Page d\u000027authentification mise à jour avec succès",
|
||||||
"healthCheckNotAvailable": "Locale",
|
"healthCheckNotAvailable": "Locale",
|
||||||
"rewritePath": "Réécrire le chemin",
|
"rewritePath": "Réécrire le chemin",
|
||||||
"rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Vedi Tutti I Siti",
|
"siteSeeAll": "Vedi Tutti I Siti",
|
||||||
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
||||||
"siteNewtCredentials": "Credenziali Newt",
|
"siteNewtCredentials": "Credenziali Newt",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Creato Il",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||||
"proxyEnableSSL": "Abilita SSL (https)",
|
"proxyEnableSSL": "Abilita SSL",
|
||||||
|
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configura Obiettivi",
|
||||||
"targetErrorFetch": "Impossibile recuperare i target",
|
"targetErrorFetch": "Impossibile recuperare i target",
|
||||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Configurazione Connessione Sicura",
|
"targetTlsSettings": "Configurazione Connessione Sicura",
|
||||||
"targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa",
|
"targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa",
|
||||||
"targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate",
|
"targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate",
|
||||||
"targetTlsSni": "Nome Server TLS (SNI)",
|
"targetTlsSni": "Nome Server Tls",
|
||||||
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
||||||
"targetTlsSubmit": "Salva Impostazioni",
|
"targetTlsSubmit": "Salva Impostazioni",
|
||||||
"targets": "Configurazione Target",
|
"targets": "Configurazione Target",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
||||||
"methodSelect": "Seleziona metodo",
|
"methodSelect": "Seleziona metodo",
|
||||||
"targetSubmit": "Aggiungi Target",
|
"targetSubmit": "Aggiungi Target",
|
||||||
"targetNoOne": "Nessun target. Aggiungi un target usando il modulo.",
|
"targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.",
|
||||||
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
||||||
"targetsSubmit": "Salva Target",
|
"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",
|
"proxyAdditional": "Impostazioni Proxy Aggiuntive",
|
||||||
"proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy",
|
"proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy",
|
||||||
"proxyCustomHeader": "Intestazione Host Personalizzata",
|
"proxyCustomHeader": "Intestazione Host Personalizzata",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Server Admin - Pangolina",
|
"pangolinServerAdmin": "Server Admin - Pangolina",
|
||||||
"licenseTierProfessional": "Licenza Professional",
|
"licenseTierProfessional": "Licenza Professional",
|
||||||
"licenseTierEnterprise": "Licenza Enterprise",
|
"licenseTierEnterprise": "Licenza Enterprise",
|
||||||
"licenseTierCommercial": "Licenza Commerciale",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Con Licenza",
|
"licensed": "Con Licenza",
|
||||||
"yes": "Sì",
|
"yes": "Sì",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Un nome visualizzato per questo provider di identità",
|
"idpDisplayName": "Un nome visualizzato per questo provider di identità",
|
||||||
"idpAutoProvisionUsers": "Provisioning Automatico Utenti",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.",
|
||||||
"licenseBadge": "Professionista",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Tipo di Provider",
|
"idpType": "Tipo di Provider",
|
||||||
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
|
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
|
||||||
"idpOidcConfigure": "Configurazione OAuth2/OIDC",
|
"idpOidcConfigure": "Configurazione OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Menu di Navigazione",
|
"navbar": "Menu di Navigazione",
|
||||||
"navbarDescription": "Menu di navigazione principale dell'applicazione",
|
"navbarDescription": "Menu di navigazione principale dell'applicazione",
|
||||||
"navbarDocsLink": "Documentazione",
|
"navbarDocsLink": "Documentazione",
|
||||||
"commercialEdition": "Edizione Commerciale",
|
|
||||||
"otpErrorEnable": "Impossibile abilitare 2FA",
|
"otpErrorEnable": "Impossibile abilitare 2FA",
|
||||||
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
|
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
|
||||||
"otpSetupCheckCode": "Inserisci un codice a 6 cifre",
|
"otpSetupCheckCode": "Inserisci un codice a 6 cifre",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Tutti Gli Utenti",
|
"sidebarAllUsers": "Tutti Gli Utenti",
|
||||||
"sidebarIdentityProviders": "Fornitori Di Identità",
|
"sidebarIdentityProviders": "Fornitori Di Identità",
|
||||||
"sidebarLicense": "Licenza",
|
"sidebarLicense": "Licenza",
|
||||||
"sidebarClients": "Clienti (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domini",
|
"sidebarDomains": "Domini",
|
||||||
"enableDockerSocket": "Abilita Progetto Docker",
|
"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.",
|
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.",
|
"twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.",
|
||||||
"twoFactor": "Autenticazione a Due Fattori",
|
"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.",
|
"adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.",
|
||||||
"continueToApplication": "Continua con l'applicazione",
|
|
||||||
"securityKeyAdd": "Aggiungi Chiave di Sicurezza",
|
"securityKeyAdd": "Aggiungi Chiave di Sicurezza",
|
||||||
"securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza",
|
"securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza",
|
||||||
"securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla",
|
"securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Proxy Esterno Abilitato",
|
"externalProxyEnabled": "Proxy Esterno Abilitato",
|
||||||
"addNewTarget": "Aggiungi Nuovo Target",
|
"addNewTarget": "Aggiungi Nuovo Target",
|
||||||
"targetsList": "Elenco dei Target",
|
"targetsList": "Elenco dei Target",
|
||||||
|
"advancedMode": "Modalità Avanzata",
|
||||||
"targetErrorDuplicateTargetFound": "Target duplicato trovato",
|
"targetErrorDuplicateTargetFound": "Target duplicato trovato",
|
||||||
"healthCheckHealthy": "Sano",
|
"healthCheckHealthy": "Sano",
|
||||||
"healthCheckUnhealthy": "Non Sano",
|
"healthCheckUnhealthy": "Non Sano",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Errore di Accesso Automatico",
|
"autoLoginError": "Errore di Accesso Automatico",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
||||||
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
|
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Gestisci Self-Hosted",
|
"remoteExitNodeManageRemoteExitNodes": "Nodi Remoti",
|
||||||
"remoteExitNodeDescription": "Gestisci i nodi per estendere la connettività di rete",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nodi",
|
"remoteExitNodes": "Nodi",
|
||||||
"searchRemoteExitNodes": "Cerca nodi...",
|
"searchRemoteExitNodes": "Cerca nodi...",
|
||||||
"remoteExitNodeAdd": "Aggiungi Nodo",
|
"remoteExitNodeAdd": "Aggiungi Nodo",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.",
|
"remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.",
|
||||||
"remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo",
|
"remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo",
|
||||||
"remoteExitNodeDelete": "Elimina Nodo",
|
"remoteExitNodeDelete": "Elimina Nodo",
|
||||||
"sidebarRemoteExitNodes": "Nodi",
|
"sidebarRemoteExitNodes": "Nodi Remoti",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Crea Nodo",
|
"title": "Crea Nodo",
|
||||||
"description": "Crea un nuovo nodo per estendere la connettività di rete",
|
"description": "Crea un nuovo nodo per estendere la connettività di rete",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Pagina di autenticazione aggiornata con successo",
|
"authPageUpdated": "Pagina di autenticazione aggiornata con successo",
|
||||||
"healthCheckNotAvailable": "Locale",
|
"healthCheckNotAvailable": "Locale",
|
||||||
"rewritePath": "Riscrivi percorso",
|
"rewritePath": "Riscrivi percorso",
|
||||||
"rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||||
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||||
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "모든 사이트 보기",
|
"siteSeeAll": "모든 사이트 보기",
|
||||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||||
"siteNewtCredentials": "Newt 자격 증명",
|
"siteNewtCredentials": "Newt 자격 증명",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "생성일",
|
"createdAt": "생성일",
|
||||||
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
|
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
|
||||||
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
|
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
|
||||||
"proxyEnableSSL": "SSL 활성화 (https)",
|
"proxyEnableSSL": "SSL 활성화",
|
||||||
|
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.",
|
||||||
|
"target": "대상",
|
||||||
|
"configureTarget": "대상 구성",
|
||||||
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
|
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
|
||||||
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
|
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
|
||||||
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
|
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "보안 연결 구성",
|
"targetTlsSettings": "보안 연결 구성",
|
||||||
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
|
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
|
||||||
"targetTlsSettingsAdvanced": "고급 TLS 설정",
|
"targetTlsSettingsAdvanced": "고급 TLS 설정",
|
||||||
"targetTlsSni": "TLS 서버 이름 (SNI)",
|
"targetTlsSni": "TLS 서버 이름",
|
||||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||||
"targetTlsSubmit": "설정 저장",
|
"targetTlsSubmit": "설정 저장",
|
||||||
"targets": "대상 구성",
|
"targets": "대상 구성",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||||
"methodSelect": "선택 방법",
|
"methodSelect": "선택 방법",
|
||||||
"targetSubmit": "대상 추가",
|
"targetSubmit": "대상 추가",
|
||||||
"targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.",
|
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.",
|
||||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||||
"targetsSubmit": "대상 저장",
|
"targetsSubmit": "대상 저장",
|
||||||
|
"addTarget": "대상 추가",
|
||||||
|
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||||
|
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||||
|
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||||
|
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
|
||||||
|
"targetErrorNoSite": "선택된 사이트 없음",
|
||||||
|
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
|
||||||
|
"targetCreated": "대상 생성",
|
||||||
|
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
|
||||||
|
"targetErrorCreate": "대상 생성 실패",
|
||||||
|
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
|
||||||
|
"save": "저장",
|
||||||
"proxyAdditional": "추가 프록시 설정",
|
"proxyAdditional": "추가 프록시 설정",
|
||||||
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
|
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
|
||||||
"proxyCustomHeader": "사용자 정의 호스트 헤더",
|
"proxyCustomHeader": "사용자 정의 호스트 헤더",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
||||||
"licenseTierProfessional": "전문 라이센스",
|
"licenseTierProfessional": "전문 라이센스",
|
||||||
"licenseTierEnterprise": "기업 라이선스",
|
"licenseTierEnterprise": "기업 라이선스",
|
||||||
"licenseTierCommercial": "상업용 라이선스",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "라이센스",
|
"licensed": "라이센스",
|
||||||
"yes": "예",
|
"yes": "예",
|
||||||
"no": "아니요",
|
"no": "아니요",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
||||||
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
||||||
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
||||||
"licenseBadge": "전문가",
|
"licenseBadge": "EE",
|
||||||
"idpType": "제공자 유형",
|
"idpType": "제공자 유형",
|
||||||
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC 구성",
|
"idpOidcConfigure": "OAuth2/OIDC 구성",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "탐색 메뉴",
|
"navbar": "탐색 메뉴",
|
||||||
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
|
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
|
||||||
"navbarDocsLink": "문서",
|
"navbarDocsLink": "문서",
|
||||||
"commercialEdition": "상업용 에디션",
|
|
||||||
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
|
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
|
||||||
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
|
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
|
||||||
"otpSetupCheckCode": "6자리 코드를 입력하세요",
|
"otpSetupCheckCode": "6자리 코드를 입력하세요",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "모든 사용자",
|
"sidebarAllUsers": "모든 사용자",
|
||||||
"sidebarIdentityProviders": "신원 공급자",
|
"sidebarIdentityProviders": "신원 공급자",
|
||||||
"sidebarLicense": "라이선스",
|
"sidebarLicense": "라이선스",
|
||||||
"sidebarClients": "클라이언트 (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "도메인",
|
"sidebarDomains": "도메인",
|
||||||
"enableDockerSocket": "Docker 청사진 활성화",
|
"enableDockerSocket": "Docker 청사진 활성화",
|
||||||
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
|
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
|
||||||
"twoFactor": "이중 인증",
|
"twoFactor": "이중 인증",
|
||||||
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
|
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
|
||||||
"continueToApplication": "응용 프로그램으로 계속",
|
|
||||||
"securityKeyAdd": "보안 키 추가",
|
"securityKeyAdd": "보안 키 추가",
|
||||||
"securityKeyRegisterTitle": "새 보안 키 등록",
|
"securityKeyRegisterTitle": "새 보안 키 등록",
|
||||||
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
|
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "외부 프록시 활성화됨",
|
"externalProxyEnabled": "외부 프록시 활성화됨",
|
||||||
"addNewTarget": "새 대상 추가",
|
"addNewTarget": "새 대상 추가",
|
||||||
"targetsList": "대상 목록",
|
"targetsList": "대상 목록",
|
||||||
|
"advancedMode": "고급 모드",
|
||||||
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
||||||
"healthCheckHealthy": "정상",
|
"healthCheckHealthy": "정상",
|
||||||
"healthCheckUnhealthy": "비정상",
|
"healthCheckUnhealthy": "비정상",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "자동 로그인 오류",
|
"autoLoginError": "자동 로그인 오류",
|
||||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "관리 자체 호스팅",
|
"remoteExitNodeManageRemoteExitNodes": "원격 노드",
|
||||||
"remoteExitNodeDescription": "네트워크 연결성을 확장하기 위해 노드를 관리하세요",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "노드",
|
"remoteExitNodes": "노드",
|
||||||
"searchRemoteExitNodes": "노드 검색...",
|
"searchRemoteExitNodes": "노드 검색...",
|
||||||
"remoteExitNodeAdd": "노드 추가",
|
"remoteExitNodeAdd": "노드 추가",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.",
|
"remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.",
|
||||||
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
|
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
|
||||||
"remoteExitNodeDelete": "노드 삭제",
|
"remoteExitNodeDelete": "노드 삭제",
|
||||||
"sidebarRemoteExitNodes": "노드",
|
"sidebarRemoteExitNodes": "원격 노드",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "노드 생성",
|
"title": "노드 생성",
|
||||||
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
|
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
|
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
|
||||||
"healthCheckNotAvailable": "로컬",
|
"healthCheckNotAvailable": "로컬",
|
||||||
"rewritePath": "경로 재작성",
|
"rewritePath": "경로 재작성",
|
||||||
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.",
|
||||||
"siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Se alle områder",
|
"siteSeeAll": "Se alle områder",
|
||||||
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
||||||
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Opprettet",
|
"createdAt": "Opprettet",
|
||||||
"proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.",
|
"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.",
|
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
|
||||||
"proxyEnableSSL": "Aktiver SSL (https)",
|
"proxyEnableSSL": "Aktiver SSL",
|
||||||
|
"proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Konfigurer mål",
|
||||||
"targetErrorFetch": "Kunne ikke hente mål",
|
"targetErrorFetch": "Kunne ikke hente mål",
|
||||||
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
|
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
|
||||||
"siteErrorFetch": "Klarte ikke å hente ressurs",
|
"siteErrorFetch": "Klarte ikke å hente ressurs",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Sikker tilkoblings-konfigurasjon",
|
"targetTlsSettings": "Sikker tilkoblings-konfigurasjon",
|
||||||
"targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din",
|
"targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din",
|
||||||
"targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger",
|
"targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger",
|
||||||
"targetTlsSni": "TLS Servernavn (SNI)",
|
"targetTlsSni": "TLS servernavn",
|
||||||
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
||||||
"targetTlsSubmit": "Lagre innstillinger",
|
"targetTlsSubmit": "Lagre innstillinger",
|
||||||
"targets": "Målkonfigurasjon",
|
"targets": "Målkonfigurasjon",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
||||||
"methodSelect": "Velg metode",
|
"methodSelect": "Velg metode",
|
||||||
"targetSubmit": "Legg til mål",
|
"targetSubmit": "Legg til mål",
|
||||||
"targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.",
|
"targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.",
|
||||||
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
||||||
"targetsSubmit": "Lagre mål",
|
"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",
|
"proxyAdditional": "Ytterligere Proxy-innstillinger",
|
||||||
"proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger",
|
"proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger",
|
||||||
"proxyCustomHeader": "Tilpasset verts-header",
|
"proxyCustomHeader": "Tilpasset verts-header",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||||
"licenseTierProfessional": "Profesjonell lisens",
|
"licenseTierProfessional": "Profesjonell lisens",
|
||||||
"licenseTierEnterprise": "Bedriftslisens",
|
"licenseTierEnterprise": "Bedriftslisens",
|
||||||
"licenseTierCommercial": "Kommersiell lisens",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Lisensiert",
|
"licensed": "Lisensiert",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nei",
|
"no": "Nei",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
|
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
|
||||||
"idpAutoProvisionUsers": "Automatisk brukerklargjøring",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
|
||||||
"licenseBadge": "Profesjonell",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Leverandørtype",
|
"idpType": "Leverandørtype",
|
||||||
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
|
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC-konfigurasjon",
|
"idpOidcConfigure": "OAuth2/OIDC-konfigurasjon",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Navigasjonsmeny",
|
"navbar": "Navigasjonsmeny",
|
||||||
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
|
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
|
||||||
"navbarDocsLink": "Dokumentasjon",
|
"navbarDocsLink": "Dokumentasjon",
|
||||||
"commercialEdition": "Kommersiell utgave",
|
|
||||||
"otpErrorEnable": "Kunne ikke aktivere 2FA",
|
"otpErrorEnable": "Kunne ikke aktivere 2FA",
|
||||||
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
|
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
|
||||||
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
|
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Alle brukere",
|
"sidebarAllUsers": "Alle brukere",
|
||||||
"sidebarIdentityProviders": "Identitetsleverandører",
|
"sidebarIdentityProviders": "Identitetsleverandører",
|
||||||
"sidebarLicense": "Lisens",
|
"sidebarLicense": "Lisens",
|
||||||
"sidebarClients": "Klienter (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domener",
|
"sidebarDomains": "Domener",
|
||||||
"enableDockerSocket": "Aktiver Docker blåkopi",
|
"enableDockerSocket": "Aktiver Docker blåkopi",
|
||||||
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.",
|
"twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.",
|
||||||
"twoFactor": "Tofaktorautentisering",
|
"twoFactor": "Tofaktorautentisering",
|
||||||
"adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.",
|
"adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.",
|
||||||
"continueToApplication": "Fortsett til applikasjonen",
|
|
||||||
"securityKeyAdd": "Legg til sikkerhetsnøkkel",
|
"securityKeyAdd": "Legg til sikkerhetsnøkkel",
|
||||||
"securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel",
|
"securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel",
|
||||||
"securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den",
|
"securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Ekstern proxy aktivert",
|
"externalProxyEnabled": "Ekstern proxy aktivert",
|
||||||
"addNewTarget": "Legg til nytt mål",
|
"addNewTarget": "Legg til nytt mål",
|
||||||
"targetsList": "Liste over mål",
|
"targetsList": "Liste over mål",
|
||||||
|
"advancedMode": "Avansert modus",
|
||||||
"targetErrorDuplicateTargetFound": "Duplikat av mål funnet",
|
"targetErrorDuplicateTargetFound": "Duplikat av mål funnet",
|
||||||
"healthCheckHealthy": "Sunn",
|
"healthCheckHealthy": "Sunn",
|
||||||
"healthCheckUnhealthy": "Usunn",
|
"healthCheckUnhealthy": "Usunn",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Feil ved automatisk innlogging",
|
"autoLoginError": "Feil ved automatisk innlogging",
|
||||||
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
|
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
|
||||||
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
|
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Administrer Selv-Hostet",
|
"remoteExitNodeManageRemoteExitNodes": "Eksterne Noder",
|
||||||
"remoteExitNodeDescription": "Administrer noder for å forlenge nettverkstilkoblingen din",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Noder",
|
"remoteExitNodes": "Noder",
|
||||||
"searchRemoteExitNodes": "Søk noder...",
|
"searchRemoteExitNodes": "Søk noder...",
|
||||||
"remoteExitNodeAdd": "Legg til Node",
|
"remoteExitNodeAdd": "Legg til Node",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.",
|
"remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.",
|
||||||
"remoteExitNodeConfirmDelete": "Bekreft sletting av Node",
|
"remoteExitNodeConfirmDelete": "Bekreft sletting av Node",
|
||||||
"remoteExitNodeDelete": "Slett Node",
|
"remoteExitNodeDelete": "Slett Node",
|
||||||
"sidebarRemoteExitNodes": "Noder",
|
"sidebarRemoteExitNodes": "Eksterne Noder",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Opprett node",
|
"title": "Opprett node",
|
||||||
"description": "Opprett en ny node for å utvide nettverkstilkoblingen din",
|
"description": "Opprett en ny node for å utvide nettverkstilkoblingen din",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Godkjenningsside oppdatert",
|
"authPageUpdated": "Godkjenningsside oppdatert",
|
||||||
"healthCheckNotAvailable": "Lokal",
|
"healthCheckNotAvailable": "Lokal",
|
||||||
"rewritePath": "Omskriv sti",
|
"rewritePath": "Omskriv sti",
|
||||||
"rewritePathDescription": "Valgfritt omskrive stien før videresending til målet."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Alle sites bekijken",
|
"siteSeeAll": "Alle sites bekijken",
|
||||||
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
||||||
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Aangemaakt op",
|
"createdAt": "Aangemaakt op",
|
||||||
"proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.",
|
"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.",
|
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
|
||||||
"proxyEnableSSL": "SSL (https) inschakelen",
|
"proxyEnableSSL": "SSL inschakelen",
|
||||||
|
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Doelstellingen configureren",
|
||||||
"targetErrorFetch": "Ophalen van doelen mislukt",
|
"targetErrorFetch": "Ophalen van doelen mislukt",
|
||||||
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
|
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
|
||||||
"siteErrorFetch": "Mislukt om resource op te halen",
|
"siteErrorFetch": "Mislukt om resource op te halen",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "HTTPS & TLS instellingen",
|
"targetTlsSettings": "HTTPS & TLS instellingen",
|
||||||
"targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren",
|
"targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren",
|
||||||
"targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen",
|
"targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen",
|
||||||
"targetTlsSni": "TLS Server Naam (SNI)",
|
"targetTlsSni": "TLS servernaam",
|
||||||
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
||||||
"targetTlsSubmit": "Instellingen opslaan",
|
"targetTlsSubmit": "Instellingen opslaan",
|
||||||
"targets": "Doelstellingen configuratie",
|
"targets": "Doelstellingen configuratie",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||||
"methodSelect": "Selecteer methode",
|
"methodSelect": "Selecteer methode",
|
||||||
"targetSubmit": "Doelwit toevoegen",
|
"targetSubmit": "Doelwit toevoegen",
|
||||||
"targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.",
|
"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.",
|
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
||||||
"targetsSubmit": "Doelstellingen opslaan",
|
"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",
|
"proxyAdditional": "Extra Proxy-instellingen",
|
||||||
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
|
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
|
||||||
"proxyCustomHeader": "Aangepaste Host-header",
|
"proxyCustomHeader": "Aangepaste Host-header",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Serverbeheer - Pangolin",
|
"pangolinServerAdmin": "Serverbeheer - Pangolin",
|
||||||
"licenseTierProfessional": "Professionele licentie",
|
"licenseTierProfessional": "Professionele licentie",
|
||||||
"licenseTierEnterprise": "Enterprise Licentie",
|
"licenseTierEnterprise": "Enterprise Licentie",
|
||||||
"licenseTierCommercial": "Commerciële licentie",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Gelicentieerd",
|
"licensed": "Gelicentieerd",
|
||||||
"yes": "ja",
|
"yes": "ja",
|
||||||
"no": "Neen",
|
"no": "Neen",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
|
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
|
||||||
"idpAutoProvisionUsers": "Auto Provisie Gebruikers",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
|
||||||
"licenseBadge": "Professioneel",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Type provider",
|
"idpType": "Type provider",
|
||||||
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
|
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC configuratie",
|
"idpOidcConfigure": "OAuth2/OIDC configuratie",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Navigatiemenu",
|
"navbar": "Navigatiemenu",
|
||||||
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
|
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
|
||||||
"navbarDocsLink": "Documentatie",
|
"navbarDocsLink": "Documentatie",
|
||||||
"commercialEdition": "Commerciële editie",
|
|
||||||
"otpErrorEnable": "Kan 2FA niet inschakelen",
|
"otpErrorEnable": "Kan 2FA niet inschakelen",
|
||||||
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
|
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
|
||||||
"otpSetupCheckCode": "Voer een 6-cijferige code in",
|
"otpSetupCheckCode": "Voer een 6-cijferige code in",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Alle gebruikers",
|
"sidebarAllUsers": "Alle gebruikers",
|
||||||
"sidebarIdentityProviders": "Identiteit aanbieders",
|
"sidebarIdentityProviders": "Identiteit aanbieders",
|
||||||
"sidebarLicense": "Licentie",
|
"sidebarLicense": "Licentie",
|
||||||
"sidebarClients": "Clients (Bèta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domeinen",
|
"sidebarDomains": "Domeinen",
|
||||||
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
||||||
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.",
|
"twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.",
|
||||||
"twoFactor": "Tweestapsverificatie",
|
"twoFactor": "Tweestapsverificatie",
|
||||||
"adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.",
|
"adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.",
|
||||||
"continueToApplication": "Doorgaan naar applicatie",
|
|
||||||
"securityKeyAdd": "Beveiligingssleutel toevoegen",
|
"securityKeyAdd": "Beveiligingssleutel toevoegen",
|
||||||
"securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren",
|
"securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren",
|
||||||
"securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren",
|
"securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Externe Proxy Ingeschakeld",
|
"externalProxyEnabled": "Externe Proxy Ingeschakeld",
|
||||||
"addNewTarget": "Voeg nieuw doelwit toe",
|
"addNewTarget": "Voeg nieuw doelwit toe",
|
||||||
"targetsList": "Lijst met doelen",
|
"targetsList": "Lijst met doelen",
|
||||||
|
"advancedMode": "Geavanceerde modus",
|
||||||
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
|
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
|
||||||
"healthCheckHealthy": "Gezond",
|
"healthCheckHealthy": "Gezond",
|
||||||
"healthCheckUnhealthy": "Ongezond",
|
"healthCheckUnhealthy": "Ongezond",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Auto Login Fout",
|
"autoLoginError": "Auto Login Fout",
|
||||||
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
|
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Beheer Zelf-Gehoste",
|
"remoteExitNodeManageRemoteExitNodes": "Externe knooppunten",
|
||||||
"remoteExitNodeDescription": "Beheer knooppunten om uw netwerkverbinding uit te breiden",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Knooppunten zoeken...",
|
"searchRemoteExitNodes": "Knooppunten zoeken...",
|
||||||
"remoteExitNodeAdd": "Voeg node toe",
|
"remoteExitNodeAdd": "Voeg node toe",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.",
|
"remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.",
|
||||||
"remoteExitNodeConfirmDelete": "Bevestig verwijderen node",
|
"remoteExitNodeConfirmDelete": "Bevestig verwijderen node",
|
||||||
"remoteExitNodeDelete": "Knoop verwijderen",
|
"remoteExitNodeDelete": "Knoop verwijderen",
|
||||||
"sidebarRemoteExitNodes": "Nodes",
|
"sidebarRemoteExitNodes": "Externe knooppunten",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Maak node",
|
"title": "Maak node",
|
||||||
"description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden",
|
"description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Auth-pagina succesvol bijgewerkt",
|
"authPageUpdated": "Auth-pagina succesvol bijgewerkt",
|
||||||
"healthCheckNotAvailable": "Lokaal",
|
"healthCheckNotAvailable": "Lokaal",
|
||||||
"rewritePath": "Herschrijf Pad",
|
"rewritePath": "Herschrijf Pad",
|
||||||
"rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
|
||||||
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Zobacz wszystkie witryny",
|
"siteSeeAll": "Zobacz wszystkie witryny",
|
||||||
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
||||||
"siteNewtCredentials": "Aktualne dane logowania",
|
"siteNewtCredentials": "Aktualne dane logowania",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Utworzono",
|
"createdAt": "Utworzono",
|
||||||
"proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.",
|
"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.",
|
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
|
||||||
"proxyEnableSSL": "Włącz SSL (https)",
|
"proxyEnableSSL": "Włącz SSL",
|
||||||
|
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Konfiguruj Targety",
|
||||||
"targetErrorFetch": "Nie udało się pobrać celów",
|
"targetErrorFetch": "Nie udało się pobrać celów",
|
||||||
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
|
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
|
||||||
"siteErrorFetch": "Nie udało się pobrać zasobu",
|
"siteErrorFetch": "Nie udało się pobrać zasobu",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Konfiguracja bezpiecznego połączenia",
|
"targetTlsSettings": "Konfiguracja bezpiecznego połączenia",
|
||||||
"targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu",
|
"targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu",
|
||||||
"targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS",
|
"targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS",
|
||||||
"targetTlsSni": "Nazwa serwera TLS (SNI)",
|
"targetTlsSni": "Nazwa serwera TLS",
|
||||||
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
||||||
"targetTlsSubmit": "Zapisz ustawienia",
|
"targetTlsSubmit": "Zapisz ustawienia",
|
||||||
"targets": "Konfiguracja celów",
|
"targets": "Konfiguracja celów",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
||||||
"methodSelect": "Wybierz metodę",
|
"methodSelect": "Wybierz metodę",
|
||||||
"targetSubmit": "Dodaj cel",
|
"targetSubmit": "Dodaj cel",
|
||||||
"targetNoOne": "Brak celów. Dodaj cel używając formularza.",
|
"targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.",
|
||||||
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
||||||
"targetsSubmit": "Zapisz cele",
|
"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",
|
"proxyAdditional": "Dodatkowe ustawienia proxy",
|
||||||
"proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy",
|
"proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy",
|
||||||
"proxyCustomHeader": "Niestandardowy nagłówek hosta",
|
"proxyCustomHeader": "Niestandardowy nagłówek hosta",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Administrator serwera - Pangolin",
|
"pangolinServerAdmin": "Administrator serwera - Pangolin",
|
||||||
"licenseTierProfessional": "Licencja Professional",
|
"licenseTierProfessional": "Licencja Professional",
|
||||||
"licenseTierEnterprise": "Licencja Enterprise",
|
"licenseTierEnterprise": "Licencja Enterprise",
|
||||||
"licenseTierCommercial": "Licencja handlowa",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Licencjonowany",
|
"licensed": "Licencjonowany",
|
||||||
"yes": "Tak",
|
"yes": "Tak",
|
||||||
"no": "Nie",
|
"no": "Nie",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
|
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
|
||||||
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.",
|
||||||
"licenseBadge": "Profesjonalny",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Typ dostawcy",
|
"idpType": "Typ dostawcy",
|
||||||
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
|
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
|
||||||
"idpOidcConfigure": "Konfiguracja OAuth2/OIDC",
|
"idpOidcConfigure": "Konfiguracja OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Menu nawigacyjne",
|
"navbar": "Menu nawigacyjne",
|
||||||
"navbarDescription": "Główne menu nawigacyjne aplikacji",
|
"navbarDescription": "Główne menu nawigacyjne aplikacji",
|
||||||
"navbarDocsLink": "Dokumentacja",
|
"navbarDocsLink": "Dokumentacja",
|
||||||
"commercialEdition": "Edycja komercyjna",
|
|
||||||
"otpErrorEnable": "Nie można włączyć 2FA",
|
"otpErrorEnable": "Nie można włączyć 2FA",
|
||||||
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
|
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
|
||||||
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
|
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Wszyscy użytkownicy",
|
"sidebarAllUsers": "Wszyscy użytkownicy",
|
||||||
"sidebarIdentityProviders": "Dostawcy tożsamości",
|
"sidebarIdentityProviders": "Dostawcy tożsamości",
|
||||||
"sidebarLicense": "Licencja",
|
"sidebarLicense": "Licencja",
|
||||||
"sidebarClients": "Klienci (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domeny",
|
"sidebarDomains": "Domeny",
|
||||||
"enableDockerSocket": "Włącz schemat dokera",
|
"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.",
|
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.",
|
"twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.",
|
||||||
"twoFactor": "Uwierzytelnianie dwuskładnikowe",
|
"twoFactor": "Uwierzytelnianie dwuskładnikowe",
|
||||||
"adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.",
|
"adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.",
|
||||||
"continueToApplication": "Kontynuuj do aplikacji",
|
|
||||||
"securityKeyAdd": "Dodaj klucz bezpieczeństwa",
|
"securityKeyAdd": "Dodaj klucz bezpieczeństwa",
|
||||||
"securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa",
|
"securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa",
|
||||||
"securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować",
|
"securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Zewnętrzny Proxy Włączony",
|
"externalProxyEnabled": "Zewnętrzny Proxy Włączony",
|
||||||
"addNewTarget": "Dodaj nowy cel",
|
"addNewTarget": "Dodaj nowy cel",
|
||||||
"targetsList": "Lista celów",
|
"targetsList": "Lista celów",
|
||||||
|
"advancedMode": "Tryb zaawansowany",
|
||||||
"targetErrorDuplicateTargetFound": "Znaleziono duplikat celu",
|
"targetErrorDuplicateTargetFound": "Znaleziono duplikat celu",
|
||||||
"healthCheckHealthy": "Zdrowy",
|
"healthCheckHealthy": "Zdrowy",
|
||||||
"healthCheckUnhealthy": "Niezdrowy",
|
"healthCheckUnhealthy": "Niezdrowy",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Błąd automatycznego logowania",
|
"autoLoginError": "Błąd automatycznego logowania",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
||||||
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
|
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Zarządzaj Samodzielnie-Hostingowane",
|
"remoteExitNodeManageRemoteExitNodes": "Zdalne węzły",
|
||||||
"remoteExitNodeDescription": "Zarządzaj węzłami w celu rozszerzenia połączenia z siecią",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Węzły",
|
"remoteExitNodes": "Węzły",
|
||||||
"searchRemoteExitNodes": "Szukaj węzłów...",
|
"searchRemoteExitNodes": "Szukaj węzłów...",
|
||||||
"remoteExitNodeAdd": "Dodaj węzeł",
|
"remoteExitNodeAdd": "Dodaj węzeł",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.",
|
"remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.",
|
||||||
"remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła",
|
"remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła",
|
||||||
"remoteExitNodeDelete": "Usuń węzeł",
|
"remoteExitNodeDelete": "Usuń węzeł",
|
||||||
"sidebarRemoteExitNodes": "Węzły",
|
"sidebarRemoteExitNodes": "Zdalne węzły",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Utwórz węzeł",
|
"title": "Utwórz węzeł",
|
||||||
"description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią",
|
"description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana",
|
"authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana",
|
||||||
"healthCheckNotAvailable": "Lokalny",
|
"healthCheckNotAvailable": "Lokalny",
|
||||||
"rewritePath": "Przepis Ścieżki",
|
"rewritePath": "Przepis Ścieżki",
|
||||||
"rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Recursos locais apenas. Sem túneis.",
|
||||||
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Ver todos os sites",
|
"siteSeeAll": "Ver todos os sites",
|
||||||
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
||||||
"siteNewtCredentials": "Credenciais Novas",
|
"siteNewtCredentials": "Credenciais Novas",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Criado Em",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
|
||||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
"proxyEnableSSL": "Habilitar SSL",
|
||||||
|
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configurar Alvos",
|
||||||
"targetErrorFetch": "Falha ao buscar alvos",
|
"targetErrorFetch": "Falha ao buscar alvos",
|
||||||
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
|
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
|
||||||
"siteErrorFetch": "Falha ao buscar recurso",
|
"siteErrorFetch": "Falha ao buscar recurso",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Configuração de conexão segura",
|
"targetTlsSettings": "Configuração de conexão segura",
|
||||||
"targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso",
|
"targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso",
|
||||||
"targetTlsSettingsAdvanced": "Configurações TLS Avançadas",
|
"targetTlsSettingsAdvanced": "Configurações TLS Avançadas",
|
||||||
"targetTlsSni": "Nome do Servidor TLS (SNI)",
|
"targetTlsSni": "Nome do Servidor TLS",
|
||||||
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
||||||
"targetTlsSubmit": "Guardar Configurações",
|
"targetTlsSubmit": "Guardar Configurações",
|
||||||
"targets": "Configuração de Alvos",
|
"targets": "Configuração de Alvos",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
||||||
"methodSelect": "Selecionar método",
|
"methodSelect": "Selecionar método",
|
||||||
"targetSubmit": "Adicionar Alvo",
|
"targetSubmit": "Adicionar Alvo",
|
||||||
"targetNoOne": "Sem alvos. Adicione um alvo usando o formulário.",
|
"targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.",
|
||||||
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
||||||
"targetsSubmit": "Guardar Alvos",
|
"targetsSubmit": "Guardar Alvos",
|
||||||
|
"addTarget": "Adicionar Alvo",
|
||||||
|
"targetErrorInvalidIp": "Endereço IP inválido",
|
||||||
|
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
||||||
|
"targetErrorInvalidPort": "Porta inválida",
|
||||||
|
"targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido",
|
||||||
|
"targetErrorNoSite": "Nenhum site selecionado",
|
||||||
|
"targetErrorNoSiteDescription": "Selecione um site para o destino",
|
||||||
|
"targetCreated": "Destino criado",
|
||||||
|
"targetCreatedDescription": "O alvo foi criado com sucesso",
|
||||||
|
"targetErrorCreate": "Falha ao criar destino",
|
||||||
|
"targetErrorCreateDescription": "Ocorreu um erro ao criar o destino",
|
||||||
|
"save": "Guardar",
|
||||||
"proxyAdditional": "Configurações Adicionais de Proxy",
|
"proxyAdditional": "Configurações Adicionais de Proxy",
|
||||||
"proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy",
|
"proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy",
|
||||||
"proxyCustomHeader": "Cabeçalho Host Personalizado",
|
"proxyCustomHeader": "Cabeçalho Host Personalizado",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
|
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
|
||||||
"licenseTierProfessional": "Licença Profissional",
|
"licenseTierProfessional": "Licença Profissional",
|
||||||
"licenseTierEnterprise": "Licença Empresarial",
|
"licenseTierEnterprise": "Licença Empresarial",
|
||||||
"licenseTierCommercial": "Licença comercial",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Licenciado",
|
"licensed": "Licenciado",
|
||||||
"yes": "Sim",
|
"yes": "Sim",
|
||||||
"no": "Não",
|
"no": "Não",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Um nome de exibição para este provedor de identidade",
|
"idpDisplayName": "Um nome de exibição para este provedor de identidade",
|
||||||
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.",
|
||||||
"licenseBadge": "Profissional",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Tipo de Provedor",
|
"idpType": "Tipo de Provedor",
|
||||||
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
|
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
|
||||||
"idpOidcConfigure": "Configuração OAuth2/OIDC",
|
"idpOidcConfigure": "Configuração OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Menu de Navegação",
|
"navbar": "Menu de Navegação",
|
||||||
"navbarDescription": "Menu de navegação principal da aplicação",
|
"navbarDescription": "Menu de navegação principal da aplicação",
|
||||||
"navbarDocsLink": "Documentação",
|
"navbarDocsLink": "Documentação",
|
||||||
"commercialEdition": "Edição Comercial",
|
|
||||||
"otpErrorEnable": "Não foi possível ativar 2FA",
|
"otpErrorEnable": "Não foi possível ativar 2FA",
|
||||||
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
|
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
|
||||||
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
|
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Todos os utilizadores",
|
"sidebarAllUsers": "Todos os utilizadores",
|
||||||
"sidebarIdentityProviders": "Provedores de identidade",
|
"sidebarIdentityProviders": "Provedores de identidade",
|
||||||
"sidebarLicense": "Tipo:",
|
"sidebarLicense": "Tipo:",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domínios",
|
"sidebarDomains": "Domínios",
|
||||||
"enableDockerSocket": "Habilitar o Diagrama Docker",
|
"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.",
|
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.",
|
"twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.",
|
||||||
"twoFactor": "Autenticação de Dois Fatores",
|
"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.",
|
"adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.",
|
||||||
"continueToApplication": "Continuar para o aplicativo",
|
|
||||||
"securityKeyAdd": "Adicionar Chave de Segurança",
|
"securityKeyAdd": "Adicionar Chave de Segurança",
|
||||||
"securityKeyRegisterTitle": "Registrar Nova 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",
|
"securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Proxy Externo Habilitado",
|
"externalProxyEnabled": "Proxy Externo Habilitado",
|
||||||
"addNewTarget": "Adicionar Novo Alvo",
|
"addNewTarget": "Adicionar Novo Alvo",
|
||||||
"targetsList": "Lista de Alvos",
|
"targetsList": "Lista de Alvos",
|
||||||
|
"advancedMode": "Modo Avançado",
|
||||||
"targetErrorDuplicateTargetFound": "Alvo duplicado encontrado",
|
"targetErrorDuplicateTargetFound": "Alvo duplicado encontrado",
|
||||||
"healthCheckHealthy": "Saudável",
|
"healthCheckHealthy": "Saudável",
|
||||||
"healthCheckUnhealthy": "Não Saudável",
|
"healthCheckUnhealthy": "Não Saudável",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Erro de Login Automático",
|
"autoLoginError": "Erro de Login Automático",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
||||||
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
|
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Gerenciar Auto-Hospedados",
|
"remoteExitNodeManageRemoteExitNodes": "Nós remotos",
|
||||||
"remoteExitNodeDescription": "Gerencie os nós para estender sua conectividade de rede",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nós",
|
"remoteExitNodes": "Nós",
|
||||||
"searchRemoteExitNodes": "Buscar nós...",
|
"searchRemoteExitNodes": "Buscar nós...",
|
||||||
"remoteExitNodeAdd": "Adicionar node",
|
"remoteExitNodeAdd": "Adicionar node",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.",
|
"remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirmar exclusão do nó",
|
"remoteExitNodeConfirmDelete": "Confirmar exclusão do nó",
|
||||||
"remoteExitNodeDelete": "Excluir nó",
|
"remoteExitNodeDelete": "Excluir nó",
|
||||||
"sidebarRemoteExitNodes": "Nós",
|
"sidebarRemoteExitNodes": "Nós remotos",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Criar nó",
|
"title": "Criar nó",
|
||||||
"description": "Crie um novo nó para estender sua conectividade de rede",
|
"description": "Crie um novo nó para estender sua conectividade de rede",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Página de autenticação atualizada com sucesso",
|
"authPageUpdated": "Página de autenticação atualizada com sucesso",
|
||||||
"healthCheckNotAvailable": "Localização",
|
"healthCheckNotAvailable": "Localização",
|
||||||
"rewritePath": "Reescrever Caminho",
|
"rewritePath": "Reescrever Caminho",
|
||||||
"rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
||||||
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||||
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
||||||
"siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Просмотреть все сайты",
|
"siteSeeAll": "Просмотреть все сайты",
|
||||||
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
||||||
"siteNewtCredentials": "Учётные данные Newt",
|
"siteNewtCredentials": "Учётные данные Newt",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Создано в",
|
"createdAt": "Создано в",
|
||||||
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
|
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
|
||||||
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
|
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
|
||||||
"proxyEnableSSL": "Включить SSL (https)",
|
"proxyEnableSSL": "Включить SSL",
|
||||||
|
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Настроить адресаты",
|
||||||
"targetErrorFetch": "Не удалось получить цели",
|
"targetErrorFetch": "Не удалось получить цели",
|
||||||
"targetErrorFetchDescription": "Произошла ошибка при получении целей",
|
"targetErrorFetchDescription": "Произошла ошибка при получении целей",
|
||||||
"siteErrorFetch": "Не удалось получить ресурс",
|
"siteErrorFetch": "Не удалось получить ресурс",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Конфигурация безопасного соединения",
|
"targetTlsSettings": "Конфигурация безопасного соединения",
|
||||||
"targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса",
|
"targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса",
|
||||||
"targetTlsSettingsAdvanced": "Расширенные настройки TLS",
|
"targetTlsSettingsAdvanced": "Расширенные настройки TLS",
|
||||||
"targetTlsSni": "Имя TLS сервера (SNI)",
|
"targetTlsSni": "Имя TLS сервера",
|
||||||
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
||||||
"targetTlsSubmit": "Сохранить настройки",
|
"targetTlsSubmit": "Сохранить настройки",
|
||||||
"targets": "Конфигурация целей",
|
"targets": "Конфигурация целей",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
|
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
|
||||||
"methodSelect": "Выберите метод",
|
"methodSelect": "Выберите метод",
|
||||||
"targetSubmit": "Добавить цель",
|
"targetSubmit": "Добавить цель",
|
||||||
"targetNoOne": "Нет целей. Добавьте цель с помощью формы.",
|
"targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы к вашему бэкэнду.",
|
||||||
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
||||||
"targetsSubmit": "Сохранить цели",
|
"targetsSubmit": "Сохранить цели",
|
||||||
|
"addTarget": "Добавить цель",
|
||||||
|
"targetErrorInvalidIp": "Неверный IP-адрес",
|
||||||
|
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
||||||
|
"targetErrorInvalidPort": "Неверный порт",
|
||||||
|
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
|
||||||
|
"targetErrorNoSite": "Сайт не выбран",
|
||||||
|
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
|
||||||
|
"targetCreated": "Цель создана",
|
||||||
|
"targetCreatedDescription": "Цель была успешно создана",
|
||||||
|
"targetErrorCreate": "Не удалось создать цель",
|
||||||
|
"targetErrorCreateDescription": "Произошла ошибка при создании цели",
|
||||||
|
"save": "Сохранить",
|
||||||
"proxyAdditional": "Дополнительные настройки прокси",
|
"proxyAdditional": "Дополнительные настройки прокси",
|
||||||
"proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси",
|
"proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси",
|
||||||
"proxyCustomHeader": "Пользовательский заголовок Host",
|
"proxyCustomHeader": "Пользовательский заголовок Host",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Администратор сервера - Pangolin",
|
"pangolinServerAdmin": "Администратор сервера - Pangolin",
|
||||||
"licenseTierProfessional": "Профессиональная лицензия",
|
"licenseTierProfessional": "Профессиональная лицензия",
|
||||||
"licenseTierEnterprise": "Корпоративная лицензия",
|
"licenseTierEnterprise": "Корпоративная лицензия",
|
||||||
"licenseTierCommercial": "Коммерческая лицензия",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Лицензировано",
|
"licensed": "Лицензировано",
|
||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
|
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
|
||||||
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
|
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
|
||||||
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
|
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
|
||||||
"licenseBadge": "Профессиональная",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Тип поставщика",
|
"idpType": "Тип поставщика",
|
||||||
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
|
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
|
||||||
"idpOidcConfigure": "Конфигурация OAuth2/OIDC",
|
"idpOidcConfigure": "Конфигурация OAuth2/OIDC",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Навигационное меню",
|
"navbar": "Навигационное меню",
|
||||||
"navbarDescription": "Главное навигационное меню приложения",
|
"navbarDescription": "Главное навигационное меню приложения",
|
||||||
"navbarDocsLink": "Документация",
|
"navbarDocsLink": "Документация",
|
||||||
"commercialEdition": "Коммерческая версия",
|
|
||||||
"otpErrorEnable": "Невозможно включить 2FA",
|
"otpErrorEnable": "Невозможно включить 2FA",
|
||||||
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
|
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
|
||||||
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
|
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Все пользователи",
|
"sidebarAllUsers": "Все пользователи",
|
||||||
"sidebarIdentityProviders": "Поставщики удостоверений",
|
"sidebarIdentityProviders": "Поставщики удостоверений",
|
||||||
"sidebarLicense": "Лицензия",
|
"sidebarLicense": "Лицензия",
|
||||||
"sidebarClients": "Клиенты (бета)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Домены",
|
"sidebarDomains": "Домены",
|
||||||
"enableDockerSocket": "Включить чертёж Docker",
|
"enableDockerSocket": "Включить чертёж Docker",
|
||||||
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
|
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
|
||||||
"twoFactor": "Двухфакторная аутентификация",
|
"twoFactor": "Двухфакторная аутентификация",
|
||||||
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
|
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
|
||||||
"continueToApplication": "Перейти к приложению",
|
|
||||||
"securityKeyAdd": "Добавить ключ безопасности",
|
"securityKeyAdd": "Добавить ключ безопасности",
|
||||||
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
|
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
|
||||||
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
|
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Внешний прокси включен",
|
"externalProxyEnabled": "Внешний прокси включен",
|
||||||
"addNewTarget": "Добавить новую цель",
|
"addNewTarget": "Добавить новую цель",
|
||||||
"targetsList": "Список целей",
|
"targetsList": "Список целей",
|
||||||
|
"advancedMode": "Расширенный режим",
|
||||||
"targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель",
|
"targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель",
|
||||||
"healthCheckHealthy": "Здоровый",
|
"healthCheckHealthy": "Здоровый",
|
||||||
"healthCheckUnhealthy": "Нездоровый",
|
"healthCheckUnhealthy": "Нездоровый",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Ошибка автоматического входа",
|
"autoLoginError": "Ошибка автоматического входа",
|
||||||
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Управление самоуправляемым",
|
"remoteExitNodeManageRemoteExitNodes": "Удаленные узлы",
|
||||||
"remoteExitNodeDescription": "Управляйте узлами для расширения сетевого подключения",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Узлы",
|
"remoteExitNodes": "Узлы",
|
||||||
"searchRemoteExitNodes": "Поиск узлов...",
|
"searchRemoteExitNodes": "Поиск узлов...",
|
||||||
"remoteExitNodeAdd": "Добавить узел",
|
"remoteExitNodeAdd": "Добавить узел",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.",
|
"remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.",
|
||||||
"remoteExitNodeConfirmDelete": "Подтвердите удаление узла",
|
"remoteExitNodeConfirmDelete": "Подтвердите удаление узла",
|
||||||
"remoteExitNodeDelete": "Удалить узел",
|
"remoteExitNodeDelete": "Удалить узел",
|
||||||
"sidebarRemoteExitNodes": "Узлы",
|
"sidebarRemoteExitNodes": "Удаленные узлы",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Создать узел",
|
"title": "Создать узел",
|
||||||
"description": "Создайте новый узел, чтобы расширить сетевое подключение",
|
"description": "Создайте новый узел, чтобы расширить сетевое подключение",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Страница авторизации успешно обновлена",
|
"authPageUpdated": "Страница авторизации успешно обновлена",
|
||||||
"healthCheckNotAvailable": "Локальный",
|
"healthCheckNotAvailable": "Локальный",
|
||||||
"rewritePath": "Переписать путь",
|
"rewritePath": "Переписать путь",
|
||||||
"rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
"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",
|
"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.",
|
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
||||||
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "Tüm Siteleri Gör",
|
"siteSeeAll": "Tüm Siteleri Gör",
|
||||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Oluşturulma Tarihi",
|
"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.",
|
"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.",
|
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
|
||||||
"proxyEnableSSL": "SSL'yi Etkinleştir (https)",
|
"proxyEnableSSL": "SSL Etkinleştir",
|
||||||
|
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
|
||||||
|
"target": "Hedef",
|
||||||
|
"configureTarget": "Hedefleri Yapılandır",
|
||||||
"targetErrorFetch": "Hedefleri alamadı",
|
"targetErrorFetch": "Hedefleri alamadı",
|
||||||
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
|
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
|
||||||
"siteErrorFetch": "kaynağa ulaşılamadı",
|
"siteErrorFetch": "kaynağa ulaşılamadı",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "HTTPS & TLS Settings",
|
"targetTlsSettings": "HTTPS & TLS Settings",
|
||||||
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
|
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
|
||||||
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
|
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
|
||||||
"targetTlsSni": "TLS Sunucu Adı (SNI)",
|
"targetTlsSni": "TLS Sunucu Adı",
|
||||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||||
"targetTlsSubmit": "Ayarları Kaydet",
|
"targetTlsSubmit": "Ayarları Kaydet",
|
||||||
"targets": "Hedefler Konfigürasyonu",
|
"targets": "Hedefler Konfigürasyonu",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||||
"methodSelect": "Yöntemi Seç",
|
"methodSelect": "Yöntemi Seç",
|
||||||
"targetSubmit": "Hedef Ekle",
|
"targetSubmit": "Hedef Ekle",
|
||||||
"targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir hedef ekleyin.",
|
"targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
|
||||||
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
||||||
"targetsSubmit": "Hedefleri Kaydet",
|
"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ı",
|
"proxyAdditional": "Ek Proxy Ayarları",
|
||||||
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
|
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
|
||||||
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
|
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
||||||
"licenseTierProfessional": "Profesyonel Lisans",
|
"licenseTierProfessional": "Profesyonel Lisans",
|
||||||
"licenseTierEnterprise": "Kurumsal Lisans",
|
"licenseTierEnterprise": "Kurumsal Lisans",
|
||||||
"licenseTierCommercial": "Ticari Lisans",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "Lisanslı",
|
"licensed": "Lisanslı",
|
||||||
"yes": "Evet",
|
"yes": "Evet",
|
||||||
"no": "Hayır",
|
"no": "Hayır",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
|
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
|
||||||
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
|
"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.",
|
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
|
||||||
"licenseBadge": "Profesyonel",
|
"licenseBadge": "EE",
|
||||||
"idpType": "Sağlayıcı Türü",
|
"idpType": "Sağlayıcı Türü",
|
||||||
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
|
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC Yapılandırması",
|
"idpOidcConfigure": "OAuth2/OIDC Yapılandırması",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "Navigasyon Menüsü",
|
"navbar": "Navigasyon Menüsü",
|
||||||
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
|
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
|
||||||
"navbarDocsLink": "Dokümantasyon",
|
"navbarDocsLink": "Dokümantasyon",
|
||||||
"commercialEdition": "Ticari Sürüm",
|
|
||||||
"otpErrorEnable": "2FA etkinleştirilemedi",
|
"otpErrorEnable": "2FA etkinleştirilemedi",
|
||||||
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
|
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
|
||||||
"otpSetupCheckCode": "6 haneli bir kod girin",
|
"otpSetupCheckCode": "6 haneli bir kod girin",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "Tüm Kullanıcılar",
|
"sidebarAllUsers": "Tüm Kullanıcılar",
|
||||||
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
|
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
|
||||||
"sidebarLicense": "Lisans",
|
"sidebarLicense": "Lisans",
|
||||||
"sidebarClients": "Müşteriler (Beta)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Alan Adları",
|
"sidebarDomains": "Alan Adları",
|
||||||
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
"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.",
|
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
|
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
|
||||||
"twoFactor": "İki Faktörlü Kimlik Doğrulama",
|
"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.",
|
"adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.",
|
||||||
"continueToApplication": "Uygulamaya Devam Et",
|
|
||||||
"securityKeyAdd": "Güvenlik Anahtarı Ekle",
|
"securityKeyAdd": "Güvenlik Anahtarı Ekle",
|
||||||
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
|
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
|
||||||
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
|
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "Dış Proxy Etkinleştirildi",
|
"externalProxyEnabled": "Dış Proxy Etkinleştirildi",
|
||||||
"addNewTarget": "Yeni Hedef Ekle",
|
"addNewTarget": "Yeni Hedef Ekle",
|
||||||
"targetsList": "Hedefler Listesi",
|
"targetsList": "Hedefler Listesi",
|
||||||
|
"advancedMode": "Gelişmiş Mod",
|
||||||
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
|
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
|
||||||
"healthCheckHealthy": "Sağlıklı",
|
"healthCheckHealthy": "Sağlıklı",
|
||||||
"healthCheckUnhealthy": "Sağlıksız",
|
"healthCheckUnhealthy": "Sağlıksız",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "Otomatik Giriş Hatası",
|
"autoLoginError": "Otomatik Giriş Hatası",
|
||||||
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
||||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
|
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Öz-Host Yönetim",
|
"remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler",
|
||||||
"remoteExitNodeDescription": "Ağ bağlantınızı genişletmek için düğümleri yönetin",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Düğümler",
|
"remoteExitNodes": "Düğümler",
|
||||||
"searchRemoteExitNodes": "Düğüm ara...",
|
"searchRemoteExitNodes": "Düğüm ara...",
|
||||||
"remoteExitNodeAdd": "Düğüm Ekle",
|
"remoteExitNodeAdd": "Düğüm Ekle",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.",
|
"remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.",
|
||||||
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
|
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
|
||||||
"remoteExitNodeDelete": "Düğümü Sil",
|
"remoteExitNodeDelete": "Düğümü Sil",
|
||||||
"sidebarRemoteExitNodes": "Düğümler",
|
"sidebarRemoteExitNodes": "Uzak Düğümler",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Düğüm Oluştur",
|
"title": "Düğüm Oluştur",
|
||||||
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
|
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
|
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
|
||||||
"healthCheckNotAvailable": "Yerel",
|
"healthCheckNotAvailable": "Yerel",
|
||||||
"rewritePath": "Yolu Yeniden Yaz",
|
"rewritePath": "Yolu Yeniden Yaz",
|
||||||
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın."
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||||
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
||||||
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
||||||
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "查看所有站点",
|
"siteSeeAll": "查看所有站点",
|
||||||
"siteTunnelDescription": "确定如何连接到您的网站",
|
"siteTunnelDescription": "确定如何连接到您的网站",
|
||||||
"siteNewtCredentials": "Newt 凭据",
|
"siteNewtCredentials": "Newt 凭据",
|
||||||
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "创建于",
|
"createdAt": "创建于",
|
||||||
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
|
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
|
||||||
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
|
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
|
||||||
"proxyEnableSSL": "启用 SSL (https)",
|
"proxyEnableSSL": "启用 SSL",
|
||||||
|
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "配置目标",
|
||||||
"targetErrorFetch": "获取目标失败",
|
"targetErrorFetch": "获取目标失败",
|
||||||
"targetErrorFetchDescription": "获取目标时出错",
|
"targetErrorFetchDescription": "获取目标时出错",
|
||||||
"siteErrorFetch": "获取资源失败",
|
"siteErrorFetch": "获取资源失败",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "安全连接配置",
|
"targetTlsSettings": "安全连接配置",
|
||||||
"targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置",
|
"targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置",
|
||||||
"targetTlsSettingsAdvanced": "高级TLS设置",
|
"targetTlsSettingsAdvanced": "高级TLS设置",
|
||||||
"targetTlsSni": "TLS 服务器名称 (SNI)",
|
"targetTlsSni": "TLS 服务器名称",
|
||||||
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
||||||
"targetTlsSubmit": "保存设置",
|
"targetTlsSubmit": "保存设置",
|
||||||
"targets": "目标配置",
|
"targets": "目标配置",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
||||||
"methodSelect": "选择方法",
|
"methodSelect": "选择方法",
|
||||||
"targetSubmit": "添加目标",
|
"targetSubmit": "添加目标",
|
||||||
"targetNoOne": "没有目标。使用表单添加目标。",
|
"targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。",
|
||||||
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
||||||
"targetsSubmit": "保存目标",
|
"targetsSubmit": "保存目标",
|
||||||
|
"addTarget": "添加目标",
|
||||||
|
"targetErrorInvalidIp": "无效的 IP 地址",
|
||||||
|
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
||||||
|
"targetErrorInvalidPort": "无效的端口",
|
||||||
|
"targetErrorInvalidPortDescription": "请输入有效的端口号",
|
||||||
|
"targetErrorNoSite": "没有选择站点",
|
||||||
|
"targetErrorNoSiteDescription": "请选择目标站点",
|
||||||
|
"targetCreated": "目标已创建",
|
||||||
|
"targetCreatedDescription": "目标已成功创建",
|
||||||
|
"targetErrorCreate": "创建目标失败",
|
||||||
|
"targetErrorCreateDescription": "创建目标时出错",
|
||||||
|
"save": "保存",
|
||||||
"proxyAdditional": "附加代理设置",
|
"proxyAdditional": "附加代理设置",
|
||||||
"proxyAdditionalDescription": "配置你的资源如何处理代理设置",
|
"proxyAdditionalDescription": "配置你的资源如何处理代理设置",
|
||||||
"proxyCustomHeader": "自定义主机标题",
|
"proxyCustomHeader": "自定义主机标题",
|
||||||
@@ -715,7 +730,7 @@
|
|||||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||||
"licenseTierProfessional": "专业许可证",
|
"licenseTierProfessional": "专业许可证",
|
||||||
"licenseTierEnterprise": "企业许可证",
|
"licenseTierEnterprise": "企业许可证",
|
||||||
"licenseTierCommercial": "商业许可证",
|
"licenseTierPersonal": "Personal License",
|
||||||
"licensed": "已授权",
|
"licensed": "已授权",
|
||||||
"yes": "是",
|
"yes": "是",
|
||||||
"no": "否",
|
"no": "否",
|
||||||
@@ -750,7 +765,7 @@
|
|||||||
"idpDisplayName": "此身份提供商的显示名称",
|
"idpDisplayName": "此身份提供商的显示名称",
|
||||||
"idpAutoProvisionUsers": "自动提供用户",
|
"idpAutoProvisionUsers": "自动提供用户",
|
||||||
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
||||||
"licenseBadge": "专业版",
|
"licenseBadge": "EE",
|
||||||
"idpType": "提供者类型",
|
"idpType": "提供者类型",
|
||||||
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
||||||
"idpOidcConfigure": "OAuth2/OIDC 配置",
|
"idpOidcConfigure": "OAuth2/OIDC 配置",
|
||||||
@@ -1084,7 +1099,6 @@
|
|||||||
"navbar": "导航菜单",
|
"navbar": "导航菜单",
|
||||||
"navbarDescription": "应用程序的主导航菜单",
|
"navbarDescription": "应用程序的主导航菜单",
|
||||||
"navbarDocsLink": "文件",
|
"navbarDocsLink": "文件",
|
||||||
"commercialEdition": "商业版",
|
|
||||||
"otpErrorEnable": "无法启用 2FA",
|
"otpErrorEnable": "无法启用 2FA",
|
||||||
"otpErrorEnableDescription": "启用 2FA 时出错",
|
"otpErrorEnableDescription": "启用 2FA 时出错",
|
||||||
"otpSetupCheckCode": "请输入您的6位数字代码",
|
"otpSetupCheckCode": "请输入您的6位数字代码",
|
||||||
@@ -1140,7 +1154,7 @@
|
|||||||
"sidebarAllUsers": "所有用户",
|
"sidebarAllUsers": "所有用户",
|
||||||
"sidebarIdentityProviders": "身份提供商",
|
"sidebarIdentityProviders": "身份提供商",
|
||||||
"sidebarLicense": "证书",
|
"sidebarLicense": "证书",
|
||||||
"sidebarClients": "客户端(测试版)",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "域",
|
"sidebarDomains": "域",
|
||||||
"enableDockerSocket": "启用 Docker 蓝图",
|
"enableDockerSocket": "启用 Docker 蓝图",
|
||||||
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
||||||
@@ -1333,7 +1347,6 @@
|
|||||||
"twoFactorRequired": "注册安全密钥需要两步验证。",
|
"twoFactorRequired": "注册安全密钥需要两步验证。",
|
||||||
"twoFactor": "两步验证",
|
"twoFactor": "两步验证",
|
||||||
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
|
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
|
||||||
"continueToApplication": "继续应用",
|
|
||||||
"securityKeyAdd": "添加安全密钥",
|
"securityKeyAdd": "添加安全密钥",
|
||||||
"securityKeyRegisterTitle": "注册新安全密钥",
|
"securityKeyRegisterTitle": "注册新安全密钥",
|
||||||
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
|
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
|
||||||
@@ -1411,6 +1424,7 @@
|
|||||||
"externalProxyEnabled": "外部代理已启用",
|
"externalProxyEnabled": "外部代理已启用",
|
||||||
"addNewTarget": "添加新目标",
|
"addNewTarget": "添加新目标",
|
||||||
"targetsList": "目标列表",
|
"targetsList": "目标列表",
|
||||||
|
"advancedMode": "高级模式",
|
||||||
"targetErrorDuplicateTargetFound": "找到重复的目标",
|
"targetErrorDuplicateTargetFound": "找到重复的目标",
|
||||||
"healthCheckHealthy": "正常",
|
"healthCheckHealthy": "正常",
|
||||||
"healthCheckUnhealthy": "不正常",
|
"healthCheckUnhealthy": "不正常",
|
||||||
@@ -1543,8 +1557,8 @@
|
|||||||
"autoLoginError": "自动登录错误",
|
"autoLoginError": "自动登录错误",
|
||||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "管理自托管",
|
"remoteExitNodeManageRemoteExitNodes": "远程节点",
|
||||||
"remoteExitNodeDescription": "管理节点以扩展您的网络连接",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "节点",
|
"remoteExitNodes": "节点",
|
||||||
"searchRemoteExitNodes": "搜索节点...",
|
"searchRemoteExitNodes": "搜索节点...",
|
||||||
"remoteExitNodeAdd": "添加节点",
|
"remoteExitNodeAdd": "添加节点",
|
||||||
@@ -1554,7 +1568,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。",
|
"remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。",
|
||||||
"remoteExitNodeConfirmDelete": "确认删除节点",
|
"remoteExitNodeConfirmDelete": "确认删除节点",
|
||||||
"remoteExitNodeDelete": "删除节点",
|
"remoteExitNodeDelete": "删除节点",
|
||||||
"sidebarRemoteExitNodes": "节点",
|
"sidebarRemoteExitNodes": "远程节点",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "创建节点",
|
"title": "创建节点",
|
||||||
"description": "创建一个新节点来扩展您的网络连接",
|
"description": "创建一个新节点来扩展您的网络连接",
|
||||||
@@ -1723,5 +1737,161 @@
|
|||||||
"authPageUpdated": "身份验证页面更新成功",
|
"authPageUpdated": "身份验证页面更新成功",
|
||||||
"healthCheckNotAvailable": "本地的",
|
"healthCheckNotAvailable": "本地的",
|
||||||
"rewritePath": "重写路径",
|
"rewritePath": "重写路径",
|
||||||
"rewritePathDescription": "在转发到目标之前,可以选择重写路径。"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
5718
package-lock.json
generated
5718
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@@ -19,21 +19,21 @@
|
|||||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts",
|
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts",
|
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts",
|
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||||
"set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
|
"set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
|
||||||
"set:pg": "echo 'export * from \"./pg\";' > 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: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",
|
"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",
|
"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",
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||||
"db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\""
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||||
"@aws-sdk/client-s3": "3.837.0",
|
"@aws-sdk/client-s3": "3.908.0",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "0.5.5",
|
"@react-email/components": "0.5.6",
|
||||||
"@react-email/render": "^1.2.0",
|
"@react-email/render": "^1.3.2",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "1.2.2",
|
||||||
"@simplewebauthn/browser": "^13.2.0",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@simplewebauthn/server": "^13.2.1",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"drizzle-orm": "0.44.6",
|
"drizzle-orm": "0.44.6",
|
||||||
"eslint": "9.35.0",
|
"eslint": "9.37.0",
|
||||||
"eslint-config-next": "15.5.4",
|
"eslint-config-next": "15.5.4",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "8.1.0",
|
"express-rate-limit": "8.1.0",
|
||||||
@@ -85,40 +85,40 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.6.1",
|
"ioredis": "5.8.1",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.545.0",
|
||||||
"maxmind": "5.0.0",
|
"maxmind": "5.0.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.3.12",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.6",
|
"nodemailer": "7.0.9",
|
||||||
"npm": "^11.6.1",
|
"npm": "^11.6.2",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"posthog-node": "^5.8.4",
|
"posthog-node": "^5.9.5",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.0",
|
||||||
"react-easy-sort": "^1.7.0",
|
"react-easy-sort": "^1.8.0",
|
||||||
"react-hook-form": "7.62.0",
|
"react-hook-form": "7.65.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"reodotdev": "^1.0.0",
|
"reodotdev": "^1.0.0",
|
||||||
"resend": "^6.1.1",
|
"resend": "^6.1.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.3",
|
||||||
"stripe": "18.2.1",
|
"stripe": "18.2.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.18.3",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.51.0",
|
"@dotenvx/dotenvx": "1.51.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "4.2.12",
|
"@react-email/preview-server": "4.3.0",
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.9",
|
||||||
@@ -139,11 +139,11 @@
|
|||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "24.6.2",
|
"@types/node": "24.7.2",
|
||||||
"@types/nodemailer": "7.0.2",
|
"@types/nodemailer": "7.0.2",
|
||||||
"@types/pg": "8.15.5",
|
"@types/pg": "8.15.5",
|
||||||
"@types/react": "19.1.16",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.2.1",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
@@ -152,12 +152,12 @@
|
|||||||
"esbuild": "0.25.10",
|
"esbuild": "0.25.10",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.2.12",
|
"react-email": "4.3.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.20.6",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.45.0"
|
"typescript-eslint": "^8.46.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |
@@ -7,21 +7,21 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware
|
notFoundMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage";
|
import { authenticated, unauthenticated } from "#dynamic/routers/external";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
|
||||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks";
|
|
||||||
import { build } from "./build";
|
import { build } from "./build";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "./types/HttpCode";
|
import HttpCode from "./types/HttpCode";
|
||||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||||
import { createStore } from "@server/lib/private/rateLimitStore";
|
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||||
import hybridRouter from "@server/routers/private/hybrid";
|
|
||||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||||
|
import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage";
|
||||||
|
import { hybridRouter } from "#dynamic/routers/hybrid";
|
||||||
|
import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks";
|
||||||
|
|
||||||
const dev = config.isDev;
|
const dev = config.isDev;
|
||||||
const externalPort = config.getRawConfig().server.external_port;
|
const externalPort = config.getRawConfig().server.external_port;
|
||||||
@@ -39,32 +39,30 @@ export function createApiServer() {
|
|||||||
apiServer.post(
|
apiServer.post(
|
||||||
`${prefix}/billing/webhooks`,
|
`${prefix}/billing/webhooks`,
|
||||||
express.raw({ type: "application/json" }),
|
express.raw({ type: "application/json" }),
|
||||||
stripeWebhookHandler
|
billingWebhookHandler
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
if (build == "oss") {
|
if (build == "oss" || !corsConfig) {
|
||||||
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)
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Using CORS options", options);
|
logger.debug("Using CORS options", options);
|
||||||
|
|
||||||
apiServer.use(cors(options));
|
apiServer.use(cors(options));
|
||||||
} else {
|
} else if (corsConfig) {
|
||||||
// Use the custom CORS middleware with loginPage support
|
// Use the custom CORS middleware with loginPage support
|
||||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { sendUsageNotification } from "@server/routers/org";
|
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
createOrgUser = "createOrgUser",
|
createOrgUser = "createOrgUser",
|
||||||
@@ -61,6 +60,7 @@ export enum ActionsEnum {
|
|||||||
getUser = "getUser",
|
getUser = "getUser",
|
||||||
setResourcePassword = "setResourcePassword",
|
setResourcePassword = "setResourcePassword",
|
||||||
setResourcePincode = "setResourcePincode",
|
setResourcePincode = "setResourcePincode",
|
||||||
|
setResourceHeaderAuth = "setResourceHeaderAuth",
|
||||||
setResourceWhitelist = "setResourceWhitelist",
|
setResourceWhitelist = "setResourceWhitelist",
|
||||||
getResourceWhitelist = "getResourceWhitelist",
|
getResourceWhitelist = "getResourceWhitelist",
|
||||||
generateAccessToken = "generateAccessToken",
|
generateAccessToken = "generateAccessToken",
|
||||||
@@ -194,7 +194,6 @@ export async function checkUserActionPermission(
|
|||||||
|
|
||||||
return roleActionPermission.length > 0;
|
return roleActionPermission.length > 0;
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking user action permission:", error);
|
console.error("Error checking user action permission:", error);
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import axios from "axios";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.session_cookie_name;
|
config.getRawConfig().server.session_cookie_name;
|
||||||
@@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
|
|||||||
token: string,
|
token: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceSessionValidationResult> {
|
): Promise<ResourceSessionValidationResult> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
|
||||||
token: token
|
|
||||||
}, await tokenManager.getAuthHeader());
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", error);
|
|
||||||
}
|
|
||||||
return { resourceSession: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
|
|||||||
13
server/cleanup.ts
Normal file
13
server/cleanup.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { cleanup as wsCleanup } from "@server/routers/ws";
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await wsCleanup();
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initCleanup() {
|
||||||
|
// Handle process termination
|
||||||
|
process.on("SIGTERM", () => cleanup());
|
||||||
|
process.on("SIGINT", () => cleanup());
|
||||||
|
}
|
||||||
@@ -35,11 +35,12 @@ function createDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create connection pools instead of individual connections
|
// Create connection pools instead of individual connections
|
||||||
|
const poolConfig = config.postgres.pool;
|
||||||
const primaryPool = new Pool({
|
const primaryPool = new Pool({
|
||||||
connectionString,
|
connectionString,
|
||||||
max: 20,
|
max: poolConfig?.max_connections || 20,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: 5000,
|
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const replicas = [];
|
const replicas = [];
|
||||||
@@ -50,9 +51,9 @@ function createDb() {
|
|||||||
for (const conn of replicaConnections) {
|
for (const conn of replicaConnections) {
|
||||||
const replicaPool = new Pool({
|
const replicaPool = new Pool({
|
||||||
connectionString: conn.connection_string,
|
connectionString: conn.connection_string,
|
||||||
max: 10,
|
max: poolConfig?.max_replica_connections || 20,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: 5000,
|
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||||
});
|
});
|
||||||
replicas.push(DrizzlePostgres(replicaPool));
|
replicas.push(DrizzlePostgres(replicaPool));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema";
|
export * from "./schema/schema";
|
||||||
export * from "./privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
pgTable,
|
pgTable,
|
||||||
serial,
|
serial,
|
||||||
@@ -381,6 +381,14 @@ export const resourcePassword = pgTable("resourcePassword", {
|
|||||||
passwordHash: varchar("passwordHash").notNull()
|
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", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -466,8 +474,6 @@ export const resourceRules = pgTable("resourceRules", {
|
|||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
templateRuleId: integer("templateRuleId")
|
|
||||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
||||||
@@ -475,40 +481,6 @@ export const resourceRules = pgTable("resourceRules", {
|
|||||||
value: varchar("value").notNull()
|
value: varchar("value").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rule templates (reusable rule sets)
|
|
||||||
export const ruleTemplates = pgTable("ruleTemplates", {
|
|
||||||
templateId: varchar("templateId").primaryKey(),
|
|
||||||
orgId: varchar("orgId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
|
||||||
name: varchar("name").notNull(),
|
|
||||||
description: varchar("description"),
|
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rules within templates
|
|
||||||
export const templateRules = pgTable("templateRules", {
|
|
||||||
ruleId: serial("ruleId").primaryKey(),
|
|
||||||
templateId: varchar("templateId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
|
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
|
||||||
priority: integer("priority").notNull(),
|
|
||||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
|
||||||
match: varchar("match").notNull(), // CIDR, IP, PATH
|
|
||||||
value: varchar("value").notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Template assignments to resources
|
|
||||||
export const resourceTemplates = pgTable("resourceTemplates", {
|
|
||||||
resourceId: integer("resourceId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
|
||||||
templateId: varchar("templateId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
|
|
||||||
});
|
|
||||||
|
|
||||||
export const supporterKey = pgTable("supporterKey", {
|
export const supporterKey = pgTable("supporterKey", {
|
||||||
keyId: serial("keyId").primaryKey(),
|
keyId: serial("keyId").primaryKey(),
|
||||||
key: varchar("key").notNull(),
|
key: varchar("key").notNull(),
|
||||||
@@ -726,6 +698,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
|||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
@@ -748,6 +721,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
|
|||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
|
||||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
ResourceRule,
|
ResourceRule,
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
|
resourceHeaderAuth,
|
||||||
|
ResourceHeaderAuth,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
@@ -15,15 +17,12 @@ import {
|
|||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import axios from "axios";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserSessionWithUser = {
|
export type UserSessionWithUser = {
|
||||||
@@ -37,30 +36,6 @@ export type UserSessionWithUser = {
|
|||||||
export async function getResourceByDomain(
|
export async function getResourceByDomain(
|
||||||
domain: string
|
domain: string
|
||||||
): Promise<ResourceWithAuth | null> {
|
): Promise<ResourceWithAuth | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -72,6 +47,10 @@ export async function getResourceByDomain(
|
|||||||
resourcePassword,
|
resourcePassword,
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
eq(resourcePassword.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourceHeaderAuth,
|
||||||
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
|
)
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -82,7 +61,8 @@ export async function getResourceByDomain(
|
|||||||
return {
|
return {
|
||||||
resource: result.resources,
|
resource: result.resources,
|
||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword
|
password: result.resourcePassword,
|
||||||
|
headerAuth: result.resourceHeaderAuth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,30 +72,6 @@ export async function getResourceByDomain(
|
|||||||
export async function getUserSessionWithUser(
|
export async function getUserSessionWithUser(
|
||||||
userSessionId: string
|
userSessionId: string
|
||||||
): Promise<UserSessionWithUser | null> {
|
): Promise<UserSessionWithUser | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
@@ -136,30 +92,6 @@ export async function getUserSessionWithUser(
|
|||||||
* Get user organization role
|
* Get user organization role
|
||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userOrgRole = await db
|
const userOrgRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
@@ -176,30 +108,6 @@ export async function getRoleResourceAccess(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleId: number
|
roleId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
@@ -221,30 +129,6 @@ export async function getUserResourceAccess(
|
|||||||
userId: string,
|
userId: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResourceAccess = await db
|
const userResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userResources)
|
.from(userResources)
|
||||||
@@ -265,30 +149,6 @@ export async function getUserResourceAccess(
|
|||||||
export async function getResourceRules(
|
export async function getResourceRules(
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceRule[]> {
|
): Promise<ResourceRule[]> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = await db
|
const rules = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceRules)
|
.from(resourceRules)
|
||||||
@@ -303,30 +163,6 @@ export async function getResourceRules(
|
|||||||
export async function getOrgLoginPage(
|
export async function getOrgLoginPage(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<LoginPage | null> {
|
): Promise<LoginPage | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(loginPageOrg)
|
.from(loginPageOrg)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema/schema";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema";
|
export * from "./schema/schema";
|
||||||
export * from "./privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
integer,
|
integer,
|
||||||
@@ -514,6 +514,16 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
|||||||
passwordHash: text("passwordHash").notNull()
|
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", {
|
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||||
accessTokenId: text("accessTokenId").primaryKey(),
|
accessTokenId: text("accessTokenId").primaryKey(),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
@@ -600,8 +610,6 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
templateRuleId: integer("templateRuleId")
|
|
||||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
||||||
@@ -609,40 +617,6 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
value: text("value").notNull()
|
value: text("value").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rule templates (reusable rule sets)
|
|
||||||
export const ruleTemplates = sqliteTable("ruleTemplates", {
|
|
||||||
templateId: text("templateId").primaryKey(),
|
|
||||||
orgId: text("orgId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
|
||||||
name: text("name").notNull(),
|
|
||||||
description: text("description"),
|
|
||||||
createdAt: integer("createdAt").notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rules within templates
|
|
||||||
export const templateRules = sqliteTable("templateRules", {
|
|
||||||
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
|
||||||
templateId: text("templateId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
|
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
||||||
priority: integer("priority").notNull(),
|
|
||||||
action: text("action").notNull(), // ACCEPT, DROP
|
|
||||||
match: text("match").notNull(), // CIDR, IP, PATH
|
|
||||||
value: text("value").notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Template assignments to resources
|
|
||||||
export const resourceTemplates = sqliteTable("resourceTemplates", {
|
|
||||||
resourceId: integer("resourceId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
|
||||||
templateId: text("templateId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
|
|
||||||
});
|
|
||||||
|
|
||||||
export const supporterKey = sqliteTable("supporterKey", {
|
export const supporterKey = sqliteTable("supporterKey", {
|
||||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
@@ -765,6 +739,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
|||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
@@ -785,6 +760,4 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
|||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
|
||||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
|
||||||
@@ -6,11 +6,6 @@ import logger from "@server/logger";
|
|||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
if (!emailConfig) {
|
if (!emailConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { render } from "@react-email/render";
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import emailClient from "@server/emails";
|
import emailClient from "@server/emails";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
export async function sendEmail(
|
export async function sendEmail(
|
||||||
template: ReactElement,
|
template: ReactElement,
|
||||||
@@ -25,7 +24,7 @@ export async function sendEmail(
|
|||||||
|
|
||||||
const emailHtml = await render(template);
|
const emailHtml = await render(template);
|
||||||
|
|
||||||
const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin";
|
const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
|
||||||
|
|
||||||
await emailClient.sendMail({
|
await emailClient.sendMail({
|
||||||
from: {
|
from: {
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
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 { themeColors } from "./lib/theme";
|
import { themeColors } from "./lib/theme";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
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 { themeColors } from "./lib/theme";
|
import { themeColors } from "./lib/theme";
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import logger from "@server/logger";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { createWebSocketClient } from "./routers/ws/client";
|
|
||||||
import { addPeer, deletePeer } from "./routers/gerbil/peers";
|
|
||||||
import { db, exitNodes } from "./db";
|
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
|
|
||||||
import { tokenManager } from "./lib/tokenManager";
|
|
||||||
import { APP_VERSION } from "./lib/consts";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export async function createHybridClientServer() {
|
|
||||||
logger.info("Starting hybrid client server...");
|
|
||||||
|
|
||||||
// Start the token manager
|
|
||||||
await tokenManager.start();
|
|
||||||
|
|
||||||
const token = await tokenManager.getToken();
|
|
||||||
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
|
|
||||||
await monitor.start();
|
|
||||||
|
|
||||||
// Create client
|
|
||||||
const client = createWebSocketClient(
|
|
||||||
token,
|
|
||||||
config.getRawConfig().managed!.endpoint!,
|
|
||||||
{
|
|
||||||
reconnectInterval: 5000,
|
|
||||||
pingInterval: 30000,
|
|
||||||
pingTimeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register message handlers
|
|
||||||
client.registerHandler("remoteExitNode/peers/add", async (message) => {
|
|
||||||
const { publicKey, allowedIps } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await addPeer(exitNode.exitNodeId, {
|
|
||||||
publicKey: publicKey,
|
|
||||||
allowedIps: allowedIps || []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
|
|
||||||
const { publicKey } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await deletePeer(exitNode.exitNodeId, publicKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-proxy-mapping
|
|
||||||
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for proxy mapping update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
|
|
||||||
logger.info(`Successfully updated proxy mapping: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating proxy mapping:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating proxy mapping:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-destinations
|
|
||||||
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for destinations update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
|
|
||||||
logger.info(`Successfully updated destinations: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating destinations:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating destinations:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
|
|
||||||
await monitor.HandleTraefikConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to connection events
|
|
||||||
client.on("connect", () => {
|
|
||||||
logger.info("Connected to WebSocket server");
|
|
||||||
client.sendMessage("remoteExitNode/register", {
|
|
||||||
remoteExitNodeVersion: APP_VERSION
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("disconnect", () => {
|
|
||||||
logger.info("Disconnected from WebSocket server");
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("message", (message) => {
|
|
||||||
logger.info(
|
|
||||||
`Received message: ${message.type} ${JSON.stringify(message.data)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
logger.info("Connection initiated");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to connect:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the ping interval stop function for cleanup if needed
|
|
||||||
const stopPingInterval = client.sendMessageInterval(
|
|
||||||
"remoteExitNode/ping",
|
|
||||||
{ timestamp: Date.now() / 1000 },
|
|
||||||
60000
|
|
||||||
); // send every minute
|
|
||||||
|
|
||||||
// Return client and cleanup function for potential use
|
|
||||||
return { client, stopPingInterval };
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,30 @@ import { runSetupFunctions } from "./setup";
|
|||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db";
|
import {
|
||||||
|
ApiKey,
|
||||||
|
ApiKeyOrg,
|
||||||
|
RemoteExitNode,
|
||||||
|
Session,
|
||||||
|
User,
|
||||||
|
UserOrg
|
||||||
|
} from "@server/db";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import { createHybridClientServer } from "./hybridServer";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
||||||
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
|
import license from "#dynamic/license/license";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
|
|
||||||
await config.initServer();
|
await config.initServer();
|
||||||
|
|
||||||
|
license.setServerSecret(config.getRawConfig().server.secret!);
|
||||||
|
await license.check();
|
||||||
|
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
|
||||||
initTelemetryClient();
|
initTelemetryClient();
|
||||||
@@ -25,16 +37,11 @@ async function startServers() {
|
|||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
|
||||||
let hybridClientServer;
|
|
||||||
let nextServer;
|
let nextServer;
|
||||||
if (config.isManagedMode()) {
|
nextServer = await createNextServer();
|
||||||
hybridClientServer = await createHybridClientServer();
|
if (config.getRawConfig().traefik.file_mode) {
|
||||||
} else {
|
const monitor = new TraefikConfigManager();
|
||||||
nextServer = await createNextServer();
|
await monitor.start();
|
||||||
if (config.getRawConfig().traefik.file_mode) {
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
await monitor.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrationServer;
|
let integrationServer;
|
||||||
@@ -42,12 +49,13 @@ async function startServers() {
|
|||||||
integrationServer = createIntegrationApiServer();
|
integrationServer = createIntegrationApiServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await initCleanup();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiServer,
|
apiServer,
|
||||||
nextServer,
|
nextServer,
|
||||||
internalServer,
|
internalServer,
|
||||||
integrationServer,
|
integrationServer
|
||||||
hybridClientServer
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware,
|
notFoundMiddleware,
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/integration";
|
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi from "swagger-ui-express";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware
|
notFoundMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import internal from "@server/routers/internal";
|
import { internalRouter } from "#dynamic/routers/internal";
|
||||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||||
|
|
||||||
const internalPort = config.getRawConfig().server.internal_port;
|
const internalPort = config.getRawConfig().server.internal_port;
|
||||||
@@ -23,7 +23,7 @@ export function createInternalServer() {
|
|||||||
internalServer.use(express.json());
|
internalServer.use(express.json());
|
||||||
|
|
||||||
const prefix = `/api/v1`;
|
const prefix = `/api/v1`;
|
||||||
internalServer.use(prefix, internal);
|
internalServer.use(prefix, internalRouter);
|
||||||
|
|
||||||
internalServer.use(notFoundMiddleware);
|
internalServer.use(notFoundMiddleware);
|
||||||
internalServer.use(errorHandlerMiddleware);
|
internalServer.use(errorHandlerMiddleware);
|
||||||
|
|||||||
6
server/lib/billing/createCustomer.ts
Normal file
6
server/lib/billing/createCustomer.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export async function createCustomer(
|
||||||
|
orgId: string,
|
||||||
|
email: string | null | undefined
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
export enum FeatureId {
|
export enum FeatureId {
|
||||||
8
server/lib/billing/getOrgTierData.ts
Normal file
8
server/lib/billing/getOrgTierData.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export async function getOrgTierData(
|
||||||
|
orgId: string
|
||||||
|
): Promise<{ tier: string | null; active: boolean }> {
|
||||||
|
let tier = null;
|
||||||
|
let active = false;
|
||||||
|
|
||||||
|
return { tier, active };
|
||||||
|
}
|
||||||
5
server/lib/billing/index.ts
Normal file
5
server/lib/billing/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./limitSet";
|
||||||
|
export * from "./features";
|
||||||
|
export * from "./limitsService";
|
||||||
|
export * from "./getOrgTierData";
|
||||||
|
export * from "./createCustomer";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FeatureId } from "./features";
|
import { FeatureId } from "./features";
|
||||||
|
|
||||||
export type LimitSet = {
|
export type LimitSet = {
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { db, limits } from "@server/db";
|
import { db, limits } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { LimitSet } from "./limitSet";
|
import { LimitSet } from "./limitSet";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export enum TierId {
|
export enum TierId {
|
||||||
STANDARD = "standard",
|
STANDARD = "standard",
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,7 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { eq, sql, and } from "drizzle-orm";
|
import { eq, sql, and } from "drizzle-orm";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { s3Client } from "../s3";
|
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import {
|
import {
|
||||||
@@ -30,10 +16,10 @@ import {
|
|||||||
Transaction
|
Transaction
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { FeatureId, getFeatureMeterId } from "./features";
|
import { FeatureId, getFeatureMeterId } from "./features";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendToClient } from "@server/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { s3Client } from "@server/lib/s3";
|
||||||
|
|
||||||
interface StripeEvent {
|
interface StripeEvent {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
@@ -45,6 +31,17 @@ interface StripeEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noop() {
|
||||||
|
if (
|
||||||
|
build !== "saas" ||
|
||||||
|
!process.env.S3_BUCKET ||
|
||||||
|
!process.env.LOCAL_FILE_PATH
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export class UsageService {
|
export class UsageService {
|
||||||
private cache: NodeCache;
|
private cache: NodeCache;
|
||||||
private bucketName: string | undefined;
|
private bucketName: string | undefined;
|
||||||
@@ -55,11 +52,13 @@ export class UsageService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket;
|
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
||||||
this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath;
|
// this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath;
|
||||||
|
this.bucketName = process.env.S3_BUCKET || undefined;
|
||||||
|
this.eventsDir = process.env.LOCAL_FILE_PATH || undefined;
|
||||||
|
|
||||||
// Ensure events directory exists
|
// Ensure events directory exists
|
||||||
this.initializeEventsDirectory().then(() => {
|
this.initializeEventsDirectory().then(() => {
|
||||||
@@ -83,7 +82,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async initializeEventsDirectory(): Promise<void> {
|
private async initializeEventsDirectory(): Promise<void> {
|
||||||
if (!this.eventsDir) {
|
if (!this.eventsDir) {
|
||||||
logger.warn("Stripe local file path is not configured, skipping events directory initialization.");
|
logger.warn(
|
||||||
|
"Stripe local file path is not configured, skipping events directory initialization."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +96,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -118,15 +121,17 @@ export class UsageService {
|
|||||||
ContentType: "application/json"
|
ContentType: "application/json"
|
||||||
});
|
});
|
||||||
await s3Client.send(uploadCommand);
|
await s3Client.send(uploadCommand);
|
||||||
|
|
||||||
// Check if file still exists before unlinking
|
// Check if file still exists before unlinking
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
|
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
|
||||||
);
|
);
|
||||||
@@ -136,7 +141,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -147,8 +154,8 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error("Failed to scan for leftover event files:", err);
|
logger.error("Failed to scan for leftover event files");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +165,17 @@ export class UsageService {
|
|||||||
value: number,
|
value: number,
|
||||||
transaction: any = null
|
transaction: any = null
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate value to 11 decimal places
|
// Truncate value to 11 decimal places
|
||||||
value = this.truncateValue(value);
|
value = this.truncateValue(value);
|
||||||
|
|
||||||
// Implement retry logic for deadlock handling
|
// Implement retry logic for deadlock handling
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
while (attempt <= maxRetries) {
|
while (attempt <= maxRetries) {
|
||||||
try {
|
try {
|
||||||
// Get subscription data for this org (with caching)
|
// Get subscription data for this org (with caching)
|
||||||
@@ -191,7 +198,12 @@ export class UsageService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
usage = await this.internalAddUsage(orgId, featureId, value, trx);
|
usage = await this.internalAddUsage(
|
||||||
|
orgId,
|
||||||
|
featureId,
|
||||||
|
value,
|
||||||
|
trx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,25 +213,26 @@ export class UsageService {
|
|||||||
return usage || null;
|
return usage || null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if this is a deadlock error
|
// Check if this is a deadlock error
|
||||||
const isDeadlock = error?.code === '40P01' ||
|
const isDeadlock =
|
||||||
error?.cause?.code === '40P01' ||
|
error?.code === "40P01" ||
|
||||||
(error?.message && error.message.includes('deadlock'));
|
error?.cause?.code === "40P01" ||
|
||||||
|
(error?.message && error.message.includes("deadlock"));
|
||||||
|
|
||||||
if (isDeadlock && attempt < maxRetries) {
|
if (isDeadlock && attempt < maxRetries) {
|
||||||
attempt++;
|
attempt++;
|
||||||
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
|
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
|
||||||
const baseDelay = Math.pow(2, attempt - 1) * 50;
|
const baseDelay = Math.pow(2, attempt - 1) * 50;
|
||||||
const jitter = Math.random() * baseDelay;
|
const jitter = Math.random() * baseDelay;
|
||||||
const delay = baseDelay + jitter;
|
const delay = baseDelay + jitter;
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
||||||
error
|
error
|
||||||
@@ -239,10 +252,10 @@ export class UsageService {
|
|||||||
): Promise<Usage> {
|
): Promise<Usage> {
|
||||||
// Truncate value to 11 decimal places
|
// Truncate value to 11 decimal places
|
||||||
value = this.truncateValue(value);
|
value = this.truncateValue(value);
|
||||||
|
|
||||||
const usageId = `${orgId}-${featureId}`;
|
const usageId = `${orgId}-${featureId}`;
|
||||||
const meterId = getFeatureMeterId(featureId);
|
const meterId = getFeatureMeterId(featureId);
|
||||||
|
|
||||||
// Use upsert: insert if not exists, otherwise increment
|
// Use upsert: insert if not exists, otherwise increment
|
||||||
const [returnUsage] = await trx
|
const [returnUsage] = await trx
|
||||||
.insert(usage)
|
.insert(usage)
|
||||||
@@ -259,7 +272,8 @@ export class UsageService {
|
|||||||
set: {
|
set: {
|
||||||
latestValue: sql`${usage.latestValue} + ${value}`
|
latestValue: sql`${usage.latestValue} + ${value}`
|
||||||
}
|
}
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
return returnUsage;
|
return returnUsage;
|
||||||
}
|
}
|
||||||
@@ -280,7 +294,7 @@ export class UsageService {
|
|||||||
value?: number,
|
value?: number,
|
||||||
customerId?: string
|
customerId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -351,7 +365,7 @@ export class UsageService {
|
|||||||
.set({
|
.set({
|
||||||
latestValue: newRunningTotal,
|
latestValue: newRunningTotal,
|
||||||
instantaneousValue: value,
|
instantaneousValue: value,
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
})
|
})
|
||||||
.where(eq(usage.usageId, usageId));
|
.where(eq(usage.usageId, usageId));
|
||||||
}
|
}
|
||||||
@@ -366,7 +380,7 @@ export class UsageService {
|
|||||||
meterId,
|
meterId,
|
||||||
instantaneousValue: truncatedValue,
|
instantaneousValue: truncatedValue,
|
||||||
latestValue: truncatedValue,
|
latestValue: truncatedValue,
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -427,7 +441,7 @@ export class UsageService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Truncate value to 11 decimal places before sending to Stripe
|
// Truncate value to 11 decimal places before sending to Stripe
|
||||||
const truncatedValue = this.truncateValue(value);
|
const truncatedValue = this.truncateValue(value);
|
||||||
|
|
||||||
const event: StripeEvent = {
|
const event: StripeEvent = {
|
||||||
identifier: uuidv4(),
|
identifier: uuidv4(),
|
||||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||||
@@ -444,7 +458,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping event file write."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -493,7 +509,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadFileToS3(): Promise<void> {
|
private async uploadFileToS3(): Promise<void> {
|
||||||
if (!this.bucketName || !this.eventsDir) {
|
if (!this.bucketName || !this.eventsDir) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping S3 upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -505,7 +523,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Check if this file is already being uploaded
|
// Check if this file is already being uploaded
|
||||||
if (this.uploadingFiles.has(fileName)) {
|
if (this.uploadingFiles.has(fileName)) {
|
||||||
logger.debug(`File ${fileName} is already being uploaded, skipping`);
|
logger.debug(
|
||||||
|
`File ${fileName} is already being uploaded, skipping`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +537,9 @@ export class UsageService {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`File ${fileName} does not exist, may have been already processed`);
|
logger.debug(
|
||||||
|
`File ${fileName} does not exist, may have been already processed`
|
||||||
|
);
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
// Reset current file if it was this file
|
// Reset current file if it was this file
|
||||||
if (this.currentEventFile === fileName) {
|
if (this.currentEventFile === fileName) {
|
||||||
@@ -537,7 +559,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted
|
// File may have been already deleted
|
||||||
logger.debug(`File ${fileName} was already deleted during cleanup`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during cleanup`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -560,7 +584,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted by another process
|
// File may have been already deleted by another process
|
||||||
logger.debug(`File ${fileName} was already deleted during upload`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -571,10 +597,7 @@ export class UsageService {
|
|||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.currentFileStartTime = 0;
|
this.currentFileStartTime = 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(`Failed to upload ${fileName} to S3:`, error);
|
||||||
`Failed to upload ${fileName} to S3:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Always remove from uploading set
|
// Always remove from uploading set
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -591,7 +614,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +633,7 @@ export class UsageService {
|
|||||||
`Creating new usage record for ${orgId}/${featureId}`
|
`Creating new usage record for ${orgId}/${featureId}`
|
||||||
);
|
);
|
||||||
const meterId = getFeatureMeterId(featureId);
|
const meterId = getFeatureMeterId(featureId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [newUsage] = await db
|
const [newUsage] = await db
|
||||||
.insert(usage)
|
.insert(usage)
|
||||||
@@ -665,7 +688,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||||
@@ -685,7 +708,9 @@ export class UsageService {
|
|||||||
*/
|
*/
|
||||||
private async uploadOldEventFiles(): Promise<void> {
|
private async uploadOldEventFiles(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping old event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -693,15 +718,17 @@ export class UsageService {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".json")) continue;
|
if (!file.endsWith(".json")) continue;
|
||||||
|
|
||||||
// Skip files that are already being uploaded
|
// Skip files that are already being uploaded
|
||||||
if (this.uploadingFiles.has(file)) {
|
if (this.uploadingFiles.has(file)) {
|
||||||
logger.debug(`Skipping file ${file} as it's already being uploaded`);
|
logger.debug(
|
||||||
|
`Skipping file ${file} as it's already being uploaded`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(this.eventsDir, file);
|
const filePath = path.join(this.eventsDir, file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file still exists before processing
|
// Check if file still exists before processing
|
||||||
try {
|
try {
|
||||||
@@ -716,7 +743,7 @@ export class UsageService {
|
|||||||
if (age >= 90000) {
|
if (age >= 90000) {
|
||||||
// 1.5 minutes - Mark as being uploaded
|
// 1.5 minutes - Mark as being uploaded
|
||||||
this.uploadingFiles.add(file);
|
this.uploadingFiles.add(file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(
|
const fileContent = await fs.readFile(
|
||||||
filePath,
|
filePath,
|
||||||
@@ -732,15 +759,17 @@ export class UsageService {
|
|||||||
ContentType: "application/json"
|
ContentType: "application/json"
|
||||||
});
|
});
|
||||||
await s3Client.send(uploadCommand);
|
await s3Client.send(uploadCommand);
|
||||||
|
|
||||||
// Check if file still exists before unlinking
|
// Check if file still exists before unlinking
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`File ${file} was already deleted during interval upload`);
|
logger.debug(
|
||||||
|
`File ${file} was already deleted during interval upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
|
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
|
||||||
);
|
);
|
||||||
@@ -755,7 +784,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -777,12 +808,17 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
public async checkLimitSet(
|
||||||
if (build !== "saas") {
|
orgId: string,
|
||||||
|
kickSites = false,
|
||||||
|
featureId?: FeatureId,
|
||||||
|
usage?: Usage
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (noop()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// This method should check the current usage against the limits set for the organization
|
// This method should check the current usage against the limits set for the organization
|
||||||
// and kick out all of the sites on the org
|
// and kick out all of the sites on the org
|
||||||
let hasExceededLimits = false;
|
let hasExceededLimits = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -817,16 +853,30 @@ export class UsageService {
|
|||||||
if (usage) {
|
if (usage) {
|
||||||
currentUsage = usage;
|
currentUsage = usage;
|
||||||
} else {
|
} else {
|
||||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
currentUsage = await this.getUsage(
|
||||||
|
orgId,
|
||||||
|
limit.featureId as FeatureId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0;
|
const usageValue =
|
||||||
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`);
|
currentUsage?.instantaneousValue ||
|
||||||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`);
|
currentUsage?.latestValue ||
|
||||||
if (currentUsage && limit.value !== null && usageValue > limit.value) {
|
0;
|
||||||
|
logger.debug(
|
||||||
|
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentUsage &&
|
||||||
|
limit.value !== null &&
|
||||||
|
usageValue > limit.value
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
||||||
`${usageValue} > ${limit.value}`
|
`${usageValue} > ${limit.value}`
|
||||||
);
|
);
|
||||||
hasExceededLimits = true;
|
hasExceededLimits = true;
|
||||||
break; // Exit early if any limit is exceeded
|
break; // Exit early if any limit is exceeded
|
||||||
@@ -835,7 +885,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// If any limits are exceeded, disconnect all sites for this organization
|
// If any limits are exceeded, disconnect all sites for this organization
|
||||||
if (hasExceededLimits && kickSites) {
|
if (hasExceededLimits && kickSites) {
|
||||||
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`);
|
logger.warn(
|
||||||
|
`Disconnecting all sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
|
|
||||||
// Get all sites for this organization
|
// Get all sites for this organization
|
||||||
const orgSites = await db
|
const orgSites = await db
|
||||||
@@ -844,7 +896,7 @@ export class UsageService {
|
|||||||
.where(eq(sites.orgId, orgId));
|
.where(eq(sites.orgId, orgId));
|
||||||
|
|
||||||
// Mark all sites as offline and send termination messages
|
// Mark all sites as offline and send termination messages
|
||||||
const siteUpdates = orgSites.map(site => site.siteId);
|
const siteUpdates = orgSites.map((site) => site.siteId);
|
||||||
|
|
||||||
if (siteUpdates.length > 0) {
|
if (siteUpdates.length > 0) {
|
||||||
// Send termination messages to newt sites
|
// Send termination messages to newt sites
|
||||||
@@ -865,17 +917,21 @@ export class UsageService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't await to prevent blocking
|
// Don't await to prevent blocking
|
||||||
sendToClient(newt.newtId, payload).catch((error: any) => {
|
sendToClient(newt.newtId, payload).catch(
|
||||||
logger.error(
|
(error: any) => {
|
||||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
logger.error(
|
||||||
error
|
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||||
);
|
error
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`);
|
logger.info(
|
||||||
|
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sendToClient } from "@server/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { processContainerLabels } from "./parseDockerContainers";
|
import { processContainerLabels } from "./parseDockerContainers";
|
||||||
import { applyBlueprint } from "./applyBlueprint";
|
import { applyBlueprint } from "./applyBlueprint";
|
||||||
import { db, sites } from "@server/db";
|
import { db, sites } from "@server/db";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
domains,
|
domains,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
Resource,
|
Resource,
|
||||||
|
resourceHeaderAuth,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourceWhitelist,
|
resourceWhitelist,
|
||||||
@@ -24,7 +25,7 @@ import {
|
|||||||
TargetData
|
TargetData
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { createCertificate } from "@server/routers/private/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import { pickPort } from "@server/routers/target/helpers";
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
import { resourcePassword } from "@server/db";
|
import { resourcePassword } from "@server/db";
|
||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
@@ -123,7 +124,9 @@ export async function updateProxyResources(
|
|||||||
|
|
||||||
const healthcheckData = targetData.healthcheck;
|
const healthcheckData = targetData.healthcheck;
|
||||||
|
|
||||||
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null;
|
const hcHeaders = healthcheckData?.headers
|
||||||
|
? JSON.stringify(healthcheckData.headers)
|
||||||
|
: null;
|
||||||
|
|
||||||
const [newHealthcheck] = await trx
|
const [newHealthcheck] = await trx
|
||||||
.insert(targetHealthCheck)
|
.insert(targetHealthCheck)
|
||||||
@@ -264,6 +267,32 @@ export async function updateProxyResources(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(resourceHeaderAuth)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceHeaderAuth.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (resourceData.auth?.["basic-auth"]) {
|
||||||
|
const headerAuthUser =
|
||||||
|
resourceData.auth?.["basic-auth"]?.user;
|
||||||
|
const headerAuthPassword =
|
||||||
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
|
if (headerAuthUser && headerAuthPassword) {
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
await trx.insert(resourceHeaderAuth).values({
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
headerAuthHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (resourceData.auth?.["sso-roles"]) {
|
if (resourceData.auth?.["sso-roles"]) {
|
||||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||||
await syncRoleResources(
|
await syncRoleResources(
|
||||||
@@ -408,7 +437,9 @@ export async function updateProxyResources(
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null;
|
const hcHeaders = healthcheckData?.headers
|
||||||
|
? JSON.stringify(healthcheckData.headers)
|
||||||
|
: null;
|
||||||
|
|
||||||
const [newHealthcheck] = await trx
|
const [newHealthcheck] = await trx
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
@@ -593,6 +624,25 @@ export async function updateProxyResources(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["basic-auth"]) {
|
||||||
|
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||||
|
const headerAuthPassword =
|
||||||
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
|
|
||||||
|
if (headerAuthUser && headerAuthPassword) {
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourceHeaderAuth).values({
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
headerAuthHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resource = newResource;
|
resource = newResource;
|
||||||
|
|
||||||
const [adminRole] = await trx
|
const [adminRole] = await trx
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export const AuthSchema = z.object({
|
|||||||
// pincode has to have 6 digits
|
// pincode has to have 6 digits
|
||||||
pincode: z.number().min(100000).max(999999).optional(),
|
pincode: z.number().min(100000).max(999999).optional(),
|
||||||
password: z.string().min(1).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-enabled": z.boolean().optional().default(false),
|
||||||
"sso-roles": z
|
"sso-roles": z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
|
|||||||
13
server/lib/certificates.ts
Normal file
13
server/lib/certificates.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
||||||
|
Array<{
|
||||||
|
id: number;
|
||||||
|
domain: string;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
certFile: string | null;
|
||||||
|
keyFile: string | null;
|
||||||
|
expiresAt: number | null;
|
||||||
|
updatedAt?: number | null;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
return []; // stub
|
||||||
|
}
|
||||||
@@ -3,19 +3,12 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { SupporterKey, supporterKey } from "@server/db";
|
import { SupporterKey, supporterKey } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { license } from "@server/license/license";
|
|
||||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
|
||||||
privateConfigSchema,
|
|
||||||
readPrivateConfigFile
|
|
||||||
} from "@server/lib/private/readConfigFile";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
private rawConfig!: z.infer<typeof configSchema>;
|
private rawConfig!: z.infer<typeof configSchema>;
|
||||||
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
|
|
||||||
|
|
||||||
supporterData: SupporterKey | null = null;
|
supporterData: SupporterKey | null = null;
|
||||||
|
|
||||||
@@ -37,19 +30,6 @@ export class Config {
|
|||||||
throw new Error(`Invalid configuration file: ${errors}`);
|
throw new Error(`Invalid configuration file: ${errors}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateEnvironment = readPrivateConfigFile();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: parsedPrivateConfig,
|
|
||||||
success: privateSuccess,
|
|
||||||
error: privateError
|
|
||||||
} = privateConfigSchema.safeParse(privateEnvironment);
|
|
||||||
|
|
||||||
if (!privateSuccess) {
|
|
||||||
const errors = fromError(privateError);
|
|
||||||
throw new Error(`Invalid private configuration file: ${errors}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
parsedConfig.users ||
|
parsedConfig.users ||
|
||||||
@@ -109,132 +89,23 @@ export class Config {
|
|||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.colors) {
|
|
||||||
process.env.BRANDING_COLORS = JSON.stringify(
|
|
||||||
parsedPrivateConfig.branding?.colors
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.logo?.light_path) {
|
|
||||||
process.env.BRANDING_LOGO_LIGHT_PATH =
|
|
||||||
parsedPrivateConfig.branding?.logo?.light_path;
|
|
||||||
}
|
|
||||||
if (parsedPrivateConfig.branding?.logo?.dark_path) {
|
|
||||||
process.env.BRANDING_LOGO_DARK_PATH =
|
|
||||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.HIDE_SUPPORTER_KEY = parsedPrivateConfig.flags
|
|
||||||
?.hide_supporter_key
|
|
||||||
? "true"
|
|
||||||
: "false";
|
|
||||||
|
|
||||||
if (build != "oss") {
|
|
||||||
if (parsedPrivateConfig.branding?.logo?.light_path) {
|
|
||||||
process.env.BRANDING_LOGO_LIGHT_PATH =
|
|
||||||
parsedPrivateConfig.branding?.logo?.light_path;
|
|
||||||
}
|
|
||||||
if (parsedPrivateConfig.branding?.logo?.dark_path) {
|
|
||||||
process.env.BRANDING_LOGO_DARK_PATH =
|
|
||||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
|
|
||||||
?.logo?.auth_page?.width
|
|
||||||
? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
|
|
||||||
: undefined;
|
|
||||||
process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
|
|
||||||
?.logo?.auth_page?.height
|
|
||||||
? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
|
|
||||||
.branding?.logo?.navbar?.width
|
|
||||||
? parsedPrivateConfig.branding?.logo?.navbar?.width.toString()
|
|
||||||
: undefined;
|
|
||||||
process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig
|
|
||||||
.branding?.logo?.navbar?.height
|
|
||||||
? parsedPrivateConfig.branding?.logo?.navbar?.height.toString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
process.env.BRANDING_FAVICON_PATH =
|
|
||||||
parsedPrivateConfig.branding?.favicon_path;
|
|
||||||
|
|
||||||
process.env.BRANDING_APP_NAME =
|
|
||||||
parsedPrivateConfig.branding?.app_name || "Pangolin";
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.footer) {
|
|
||||||
process.env.BRANDING_FOOTER = JSON.stringify(
|
|
||||||
parsedPrivateConfig.branding?.footer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.LOGIN_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.login_page?.title_text || "";
|
|
||||||
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.login_page?.subtitle_text || "";
|
|
||||||
|
|
||||||
process.env.SIGNUP_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.signup_page?.title_text || "";
|
|
||||||
process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.signup_page?.subtitle_text || "";
|
|
||||||
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page
|
|
||||||
?.hide_powered_by === true
|
|
||||||
? "true"
|
|
||||||
: "false";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page?.show_logo ===
|
|
||||||
true
|
|
||||||
? "true"
|
|
||||||
: "false";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page?.title_text ||
|
|
||||||
"";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page
|
|
||||||
?.subtitle_text || "";
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.background_image_path) {
|
|
||||||
process.env.BACKGROUND_IMAGE_PATH =
|
|
||||||
parsedPrivateConfig.branding?.background_image_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.server.reo_client_id) {
|
|
||||||
process.env.REO_CLIENT_ID =
|
|
||||||
parsedPrivateConfig.server.reo_client_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedConfig.server.maxmind_db_path) {
|
if (parsedConfig.server.maxmind_db_path) {
|
||||||
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
|
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rawConfig = parsedConfig;
|
this.rawConfig = parsedConfig;
|
||||||
this.rawPrivateConfig = parsedPrivateConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initServer() {
|
public async initServer() {
|
||||||
if (!this.rawConfig) {
|
if (!this.rawConfig) {
|
||||||
throw new Error("Config not loaded. Call load() first.");
|
throw new Error("Config not loaded. Call load() first.");
|
||||||
}
|
}
|
||||||
if (this.rawConfig.managed) {
|
|
||||||
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
license.setServerSecret(this.rawConfig.server.secret!);
|
|
||||||
|
|
||||||
await this.checkKeyStatus();
|
await this.checkKeyStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkKeyStatus() {
|
private async checkKeyStatus() {
|
||||||
const licenseStatus = await license.check();
|
if (build == "oss") {
|
||||||
if (
|
|
||||||
!this.rawPrivateConfig.flags?.hide_supporter_key &&
|
|
||||||
build != "oss" &&
|
|
||||||
!licenseStatus.isHostLicensed
|
|
||||||
) {
|
|
||||||
this.checkSupporterKey();
|
this.checkSupporterKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,10 +114,6 @@ export class Config {
|
|||||||
return this.rawConfig;
|
return this.rawConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRawPrivateConfig() {
|
|
||||||
return this.rawPrivateConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNoReplyEmail(): string | undefined {
|
public getNoReplyEmail(): string | undefined {
|
||||||
return (
|
return (
|
||||||
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
|
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
|
||||||
@@ -280,10 +147,6 @@ export class Config {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isManagedMode() {
|
|
||||||
return typeof this.rawConfig?.managed === "object";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkSupporterKey() {
|
public async checkSupporterKey() {
|
||||||
const [key] = await db.select().from(supporterKey).limit(1);
|
const [key] = await db.select().from(supporterKey).limit(1);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.10.4";
|
export const APP_VERSION = "1.11.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import cors, { CorsOptions } from "cors";
|
import cors, { CorsOptions } from "cors";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -1,16 +1,3 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { isValidCIDR } from "@server/lib/validators";
|
import { isValidCIDR } from "@server/lib/validators";
|
||||||
import { getNextAvailableOrgSubnet } from "@server/lib/ip";
|
import { getNextAvailableOrgSubnet } from "@server/lib/ip";
|
||||||
import {
|
import {
|
||||||
@@ -28,9 +15,9 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role";
|
import { defaultRoleAllowedActions } from "@server/routers/role";
|
||||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/private/billing";
|
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
||||||
import { createCustomer } from "@server/routers/private/billing/createCustomer";
|
import { createCustomer } from "#dynamic/lib/billing";
|
||||||
import { usageService } from "@server/lib/private/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
|
|
||||||
export async function createUserAccountOrg(
|
export async function createUserAccountOrg(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess(
|
|||||||
return { hasAccess: true, exitNode };
|
return { hasAccess: true, exitNode };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listExitNodes(orgId: string, filterOnline = false, noCloud = false) {
|
export async function listExitNodes(
|
||||||
|
orgId: string,
|
||||||
|
filterOnline = false,
|
||||||
|
noCloud = false
|
||||||
|
) {
|
||||||
// TODO: pick which nodes to send and ping better than just all of them that are not remote
|
// TODO: pick which nodes to send and ping better than just all of them that are not remote
|
||||||
const allExitNodes = await db
|
const allExitNodes = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -59,7 +63,16 @@ export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveExitNodes(hostname: string, publicKey: string) {
|
export async function resolveExitNodes(
|
||||||
|
hostname: string,
|
||||||
|
publicKey: string
|
||||||
|
): Promise<
|
||||||
|
{
|
||||||
|
endpoint: string;
|
||||||
|
publicKey: string;
|
||||||
|
orgId: string;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
// OSS version: simple implementation that returns empty array
|
// OSS version: simple implementation that returns empty array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
import { build } from "@server/build";
|
export * from "./exitNodes";
|
||||||
|
export * from "./exitNodeComms";
|
||||||
// Import both modules
|
|
||||||
import * as exitNodesModule from "./exitNodes";
|
|
||||||
import * as privateExitNodesModule from "./privateExitNodes";
|
|
||||||
|
|
||||||
// Conditionally export exit nodes implementation based on build type
|
|
||||||
const exitNodesImplementation = build === "oss" ? exitNodesModule : privateExitNodesModule;
|
|
||||||
|
|
||||||
// Re-export all items from the selected implementation
|
|
||||||
export const {
|
|
||||||
verifyExitNodeOrgAccess,
|
|
||||||
listExitNodes,
|
|
||||||
selectBestExitNode,
|
|
||||||
checkExitNodeOrg,
|
|
||||||
resolveExitNodes
|
|
||||||
} = exitNodesImplementation;
|
|
||||||
|
|
||||||
// Import communications modules
|
|
||||||
import * as exitNodeCommsModule from "./exitNodeComms";
|
|
||||||
import * as privateExitNodeCommsModule from "./privateExitNodeComms";
|
|
||||||
|
|
||||||
// Conditionally export communications implementation based on build type
|
|
||||||
const exitNodeCommsImplementation = build === "oss" ? exitNodeCommsModule : privateExitNodeCommsModule;
|
|
||||||
|
|
||||||
// Re-export communications functions from the selected implementation
|
|
||||||
export const {
|
|
||||||
sendToExitNode
|
|
||||||
} = exitNodeCommsImplementation;
|
|
||||||
|
|
||||||
// Re-export shared modules
|
|
||||||
export * from "./subnet";
|
export * from "./subnet";
|
||||||
export * from "./getCurrentExitNodeId";
|
export * from "./getCurrentExitNodeId";
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import axios from "axios";
|
|
||||||
import config from "./config";
|
|
||||||
import { tokenManager } from "./tokenManager";
|
|
||||||
|
|
||||||
export async function getCountryCodeForIp(
|
export async function getCountryCodeForIp(
|
||||||
ip: string
|
ip: string
|
||||||
@@ -33,32 +30,4 @@ export async function getCountryCodeForIp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remoteGetCountryCodeForIp(
|
|
||||||
ip: string
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data.data.countryCode;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import RedisStore from "@server/db/private/redisStore";
|
|
||||||
import { MemoryStore, Store } from "express-rate-limit";
|
|
||||||
|
|
||||||
export function createStore(): Store {
|
|
||||||
const rateLimitStore: Store = new RedisStore({
|
|
||||||
prefix: 'api-rate-limit', // Optional: customize Redis key prefix
|
|
||||||
skipFailedRequests: true, // Don't count failed requests
|
|
||||||
skipSuccessfulRequests: false, // Count successful requests
|
|
||||||
});
|
|
||||||
|
|
||||||
return rateLimitStore;
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from "fs";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import { privateConfigFilePath1 } from "@server/lib/consts";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { colorsSchema } from "@server/lib/colorsSchema";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
|
||||||
|
|
||||||
export const privateConfigSchema = z
|
|
||||||
.object({
|
|
||||||
app: z.object({
|
|
||||||
region: z.string().optional().default("default"),
|
|
||||||
base_domain: z.string().optional()
|
|
||||||
}).optional().default({
|
|
||||||
region: "default"
|
|
||||||
}),
|
|
||||||
server: z.object({
|
|
||||||
encryption_key_path: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("./config/encryption.pem")
|
|
||||||
.pipe(z.string().min(8)),
|
|
||||||
resend_api_key: z.string().optional(),
|
|
||||||
reo_client_id: z.string().optional(),
|
|
||||||
}).optional().default({
|
|
||||||
encryption_key_path: "./config/encryption.pem"
|
|
||||||
}),
|
|
||||||
redis: z
|
|
||||||
.object({
|
|
||||||
host: z.string(),
|
|
||||||
port: portSchema,
|
|
||||||
password: z.string().optional(),
|
|
||||||
db: z.number().int().nonnegative().optional().default(0),
|
|
||||||
replicas: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
host: z.string(),
|
|
||||||
port: portSchema,
|
|
||||||
password: z.string().optional(),
|
|
||||||
db: z.number().int().nonnegative().optional().default(0)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
// tls: z
|
|
||||||
// .object({
|
|
||||||
// reject_unauthorized: z
|
|
||||||
// .boolean()
|
|
||||||
// .optional()
|
|
||||||
// .default(true)
|
|
||||||
// })
|
|
||||||
// .optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
gerbil: z
|
|
||||||
.object({
|
|
||||||
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
flags: z
|
|
||||||
.object({
|
|
||||||
enable_redis: z.boolean().optional(),
|
|
||||||
hide_supporter_key: z.boolean().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
branding: z
|
|
||||||
.object({
|
|
||||||
app_name: z.string().optional(),
|
|
||||||
background_image_path: z.string().optional(),
|
|
||||||
colors: z
|
|
||||||
.object({
|
|
||||||
light: colorsSchema.optional(),
|
|
||||||
dark: colorsSchema.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
logo: z
|
|
||||||
.object({
|
|
||||||
light_path: z.string().optional(),
|
|
||||||
dark_path: z.string().optional(),
|
|
||||||
auth_page: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
navbar: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
favicon_path: z.string().optional(),
|
|
||||||
footer: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
text: z.string(),
|
|
||||||
href: z.string().optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
login_page: z
|
|
||||||
.object({
|
|
||||||
subtitle_text: z.string().optional(),
|
|
||||||
title_text: z.string().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
signup_page: z
|
|
||||||
.object({
|
|
||||||
subtitle_text: z.string().optional(),
|
|
||||||
title_text: z.string().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
resource_auth_page: z
|
|
||||||
.object({
|
|
||||||
show_logo: z.boolean().optional(),
|
|
||||||
hide_powered_by: z.boolean().optional(),
|
|
||||||
title_text: z.string().optional(),
|
|
||||||
subtitle_text: z.string().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
emails: z
|
|
||||||
.object({
|
|
||||||
signature: z.string().optional(),
|
|
||||||
colors: z
|
|
||||||
.object({
|
|
||||||
primary: z.string().optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
stripe: z
|
|
||||||
.object({
|
|
||||||
secret_key: z.string(),
|
|
||||||
webhook_secret: z.string(),
|
|
||||||
s3Bucket: z.string(),
|
|
||||||
s3Region: z.string().default("us-east-1"),
|
|
||||||
localFilePath: z.string()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function readPrivateConfigFile() {
|
|
||||||
if (build == "oss") {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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(privateConfigFilePath1)) {
|
|
||||||
environment = loadConfig(privateConfigFilePath1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!environment) {
|
|
||||||
throw new Error(
|
|
||||||
"No private configuration file found."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return environment;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { S3Client } from "@aws-sdk/client-s3";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
export const s3Client = new S3Client({
|
|
||||||
region: config.getRawPrivateConfig().stripe?.s3Region || "us-east-1",
|
|
||||||
});
|
|
||||||
@@ -12,42 +12,36 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
|||||||
|
|
||||||
export const configSchema = z
|
export const configSchema = z
|
||||||
.object({
|
.object({
|
||||||
app: z.object({
|
app: z
|
||||||
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),
|
|
||||||
telemetry: z
|
|
||||||
.object({
|
|
||||||
anonymous_usage: z.boolean().optional().default(true)
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
}).optional().default({
|
|
||||||
log_level: "info",
|
|
||||||
save_logs: false,
|
|
||||||
log_failed_attempts: false,
|
|
||||||
telemetry: {
|
|
||||||
anonymous_usage: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
managed: z
|
|
||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
dashboard_url: z
|
||||||
id: z.string().optional(),
|
.string()
|
||||||
secret: z.string().optional(),
|
.url()
|
||||||
endpoint: z.string().optional().default("https://pangolin.fossorial.io"),
|
.pipe(z.string().url())
|
||||||
redirect_endpoint: z.string().optional()
|
.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),
|
||||||
|
telemetry: z
|
||||||
|
.object({
|
||||||
|
anonymous_usage: z.boolean().optional().default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({})
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
|
.default({
|
||||||
|
log_level: "info",
|
||||||
|
save_logs: false,
|
||||||
|
log_failed_attempts: false,
|
||||||
|
telemetry: {
|
||||||
|
anonymous_usage: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
domains: z
|
domains: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -61,94 +55,95 @@ export const configSchema = z
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
server: z.object({
|
server: z
|
||||||
integration_port: portSchema
|
.object({
|
||||||
.optional()
|
integration_port: portSchema
|
||||||
.default(3003)
|
.optional()
|
||||||
.transform(stoi)
|
.default(3003)
|
||||||
.pipe(portSchema.optional()),
|
.transform(stoi)
|
||||||
external_port: portSchema
|
.pipe(portSchema.optional()),
|
||||||
.optional()
|
external_port: portSchema
|
||||||
.default(3000)
|
.optional()
|
||||||
.transform(stoi)
|
.default(3000)
|
||||||
.pipe(portSchema),
|
.transform(stoi)
|
||||||
internal_port: portSchema
|
.pipe(portSchema),
|
||||||
.optional()
|
internal_port: portSchema
|
||||||
.default(3001)
|
.optional()
|
||||||
.transform(stoi)
|
.default(3001)
|
||||||
.pipe(portSchema),
|
.transform(stoi)
|
||||||
next_port: portSchema
|
.pipe(portSchema),
|
||||||
.optional()
|
next_port: portSchema
|
||||||
.default(3002)
|
.optional()
|
||||||
.transform(stoi)
|
.default(3002)
|
||||||
.pipe(portSchema),
|
.transform(stoi)
|
||||||
internal_hostname: z
|
.pipe(portSchema),
|
||||||
.string()
|
internal_hostname: z
|
||||||
.optional()
|
.string()
|
||||||
.default("pangolin")
|
.optional()
|
||||||
.transform((url) => url.toLowerCase()),
|
.default("pangolin")
|
||||||
session_cookie_name: z
|
.transform((url) => url.toLowerCase()),
|
||||||
.string()
|
session_cookie_name: z
|
||||||
.optional()
|
.string()
|
||||||
.default("p_session_token"),
|
.optional()
|
||||||
resource_access_token_param: z
|
.default("p_session_token"),
|
||||||
.string()
|
resource_access_token_param: z
|
||||||
.optional()
|
.string()
|
||||||
.default("p_token"),
|
.optional()
|
||||||
resource_access_token_headers: z
|
.default("p_token"),
|
||||||
.object({
|
resource_access_token_headers: z
|
||||||
id: z.string().optional().default("P-Access-Token-Id"),
|
.object({
|
||||||
token: z.string().optional().default("P-Access-Token")
|
id: z.string().optional().default("P-Access-Token-Id"),
|
||||||
})
|
token: z.string().optional().default("P-Access-Token")
|
||||||
.optional()
|
})
|
||||||
.default({}),
|
.optional()
|
||||||
resource_session_request_param: z
|
.default({}),
|
||||||
.string()
|
resource_session_request_param: z
|
||||||
.optional()
|
.string()
|
||||||
.default("resource_session_request_param"),
|
.optional()
|
||||||
dashboard_session_length_hours: z
|
.default("resource_session_request_param"),
|
||||||
.number()
|
dashboard_session_length_hours: z
|
||||||
.positive()
|
.number()
|
||||||
.gt(0)
|
.positive()
|
||||||
.optional()
|
.gt(0)
|
||||||
.default(720),
|
.optional()
|
||||||
resource_session_length_hours: z
|
.default(720),
|
||||||
.number()
|
resource_session_length_hours: z
|
||||||
.positive()
|
.number()
|
||||||
.gt(0)
|
.positive()
|
||||||
.optional()
|
.gt(0)
|
||||||
.default(720),
|
.optional()
|
||||||
cors: z
|
.default(720),
|
||||||
.object({
|
cors: z
|
||||||
origins: z.array(z.string()).optional(),
|
.object({
|
||||||
methods: z.array(z.string()).optional(),
|
origins: z.array(z.string()).optional(),
|
||||||
allowed_headers: z.array(z.string()).optional(),
|
methods: z.array(z.string()).optional(),
|
||||||
credentials: z.boolean().optional()
|
allowed_headers: z.array(z.string()).optional(),
|
||||||
})
|
credentials: z.boolean().optional()
|
||||||
.optional(),
|
})
|
||||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
.optional(),
|
||||||
secret: z
|
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||||
.string()
|
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||||
.pipe(z.string().min(8))
|
maxmind_db_path: z.string().optional()
|
||||||
.optional(),
|
})
|
||||||
maxmind_db_path: z.string().optional()
|
.optional()
|
||||||
}).optional().default({
|
.default({
|
||||||
integration_port: 3003,
|
integration_port: 3003,
|
||||||
external_port: 3000,
|
external_port: 3000,
|
||||||
internal_port: 3001,
|
internal_port: 3001,
|
||||||
next_port: 3002,
|
next_port: 3002,
|
||||||
internal_hostname: "pangolin",
|
internal_hostname: "pangolin",
|
||||||
session_cookie_name: "p_session_token",
|
session_cookie_name: "p_session_token",
|
||||||
resource_access_token_param: "p_token",
|
resource_access_token_param: "p_token",
|
||||||
resource_access_token_headers: {
|
resource_access_token_headers: {
|
||||||
id: "P-Access-Token-Id",
|
id: "P-Access-Token-Id",
|
||||||
token: "P-Access-Token"
|
token: "P-Access-Token"
|
||||||
},
|
},
|
||||||
resource_session_request_param: "resource_session_request_param",
|
resource_session_request_param:
|
||||||
dashboard_session_length_hours: 720,
|
"resource_session_request_param",
|
||||||
resource_session_length_hours: 720,
|
dashboard_session_length_hours: 720,
|
||||||
trust_proxy: 1
|
resource_session_length_hours: 720,
|
||||||
}),
|
trust_proxy: 1
|
||||||
|
}),
|
||||||
postgres: z
|
postgres: z
|
||||||
.object({
|
.object({
|
||||||
connection_string: z.string().optional(),
|
connection_string: z.string().optional(),
|
||||||
@@ -158,7 +153,32 @@ export const configSchema = z
|
|||||||
connection_string: z.string()
|
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()
|
.optional()
|
||||||
|
.default({})
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
traefik: z
|
traefik: z
|
||||||
@@ -179,7 +199,10 @@ export const configSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.default("/var/dynamic/router_config.yml"),
|
.default("/var/dynamic/router_config.yml"),
|
||||||
static_domains: z.array(z.string()).optional().default([]),
|
static_domains: z.array(z.string()).optional().default([]),
|
||||||
site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]),
|
site_types: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.default(["newt", "wireguard", "local"]),
|
||||||
allow_raw_resources: z.boolean().optional().default(true),
|
allow_raw_resources: z.boolean().optional().default(true),
|
||||||
file_mode: z.boolean().optional().default(false)
|
file_mode: z.boolean().optional().default(false)
|
||||||
})
|
})
|
||||||
@@ -306,10 +329,7 @@ export const configSchema = z
|
|||||||
if (data.flags?.disable_config_managed_domains) {
|
if (data.flags?.disable_config_managed_domains) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// If hybrid is defined, domains are not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -321,15 +341,14 @@ export const configSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// If hybrid is defined, server secret is not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
|
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
|
||||||
if (data.server?.secret === undefined) {
|
if (data.server?.secret === undefined) {
|
||||||
data.server.secret = process.env.SERVER_SECRET;
|
data.server.secret = process.env.SERVER_SECRET;
|
||||||
}
|
}
|
||||||
return data.server?.secret !== undefined && data.server.secret.length > 0;
|
return (
|
||||||
|
data.server?.secret !== undefined &&
|
||||||
|
data.server.secret.length > 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Server secret must be defined"
|
message: "Server secret must be defined"
|
||||||
@@ -337,12 +356,11 @@ export const configSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// If hybrid is defined, dashboard_url is not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If hybrid is not defined, dashboard_url must be defined
|
// If hybrid is not defined, dashboard_url must be defined
|
||||||
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
return (
|
||||||
|
data.app.dashboard_url !== undefined &&
|
||||||
|
data.app.dashboard_url.length > 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Dashboard URL must be defined"
|
message: "Dashboard URL must be defined"
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { tokenManager } from "../tokenManager";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import config from "../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get valid certificates for the specified domains
|
|
||||||
*/
|
|
||||||
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
if (domains.size === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const domainArray = Array.from(domains);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
domains: domainArray
|
|
||||||
},
|
|
||||||
headers: (await tokenManager.getAuthHeader()).headers
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
|
|
||||||
{ responseData: response.data, domains: domainArray }
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.debug(
|
|
||||||
// `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
|
|
||||||
// );
|
|
||||||
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error getting certificates:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error getting certificates:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
return []; // stub
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
// Import both modules
|
|
||||||
import * as certificateModule from "./certificates";
|
|
||||||
import * as privateCertificateModule from "./privateCertificates";
|
|
||||||
|
|
||||||
// Conditionally export Remote Certificates implementation based on build type
|
|
||||||
const remoteCertificatesImplementation = build === "oss" ? certificateModule : privateCertificateModule;
|
|
||||||
|
|
||||||
// Re-export all items from the selected implementation
|
|
||||||
export const {
|
|
||||||
getValidCertificatesForDomains,
|
|
||||||
getValidCertificatesForDomainsHybrid
|
|
||||||
} = remoteCertificatesImplementation;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import config from "../config";
|
|
||||||
import { certificates, db } from "@server/db";
|
|
||||||
import { and, eq, isNotNull } from "drizzle-orm";
|
|
||||||
import { decryptData } from "../encryption";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export async function getValidCertificatesForDomains(
|
|
||||||
domains: Set<string>
|
|
||||||
): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
if (domains.size === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const domainArray = Array.from(domains);
|
|
||||||
|
|
||||||
// TODO: add more foreign keys to make this query more efficient - we dont need to keep getting every certificate
|
|
||||||
const validCerts = await db
|
|
||||||
.select({
|
|
||||||
id: certificates.certId,
|
|
||||||
domain: certificates.domain,
|
|
||||||
certFile: certificates.certFile,
|
|
||||||
keyFile: certificates.keyFile,
|
|
||||||
expiresAt: certificates.expiresAt,
|
|
||||||
updatedAt: certificates.updatedAt,
|
|
||||||
wildcard: certificates.wildcard
|
|
||||||
})
|
|
||||||
.from(certificates)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(certificates.status, "valid"),
|
|
||||||
isNotNull(certificates.certFile),
|
|
||||||
isNotNull(certificates.keyFile)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter certificates for the specified domains and if it is a wildcard then you can match on everything up to the first dot
|
|
||||||
const validCertsFiltered = validCerts.filter((cert) => {
|
|
||||||
return (
|
|
||||||
domainArray.includes(cert.domain) ||
|
|
||||||
(cert.wildcard &&
|
|
||||||
domainArray.some((domain) =>
|
|
||||||
domain.endsWith(`.${cert.domain}`)
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path;
|
|
||||||
|
|
||||||
if (!fs.existsSync(encryptionKeyPath)) {
|
|
||||||
throw new Error(
|
|
||||||
"Encryption key file not found. Please generate one first."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
|
||||||
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
|
||||||
|
|
||||||
const validCertsDecrypted = validCertsFiltered.map((cert) => {
|
|
||||||
// Decrypt and save certificate file
|
|
||||||
const decryptedCert = decryptData(
|
|
||||||
cert.certFile!, // is not null from query
|
|
||||||
encryptionKey
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decrypt and save key file
|
|
||||||
const decryptedKey = decryptData(cert.keyFile!, encryptionKey);
|
|
||||||
|
|
||||||
// Return only the certificate data without org information
|
|
||||||
return {
|
|
||||||
...cert,
|
|
||||||
certFile: decryptedCert,
|
|
||||||
keyFile: decryptedKey
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return validCertsDecrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getValidCertificatesForDomainsHybrid(
|
|
||||||
domains: Set<string>
|
|
||||||
): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
return []; // stub
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { Router } from "express";
|
|
||||||
import axios from "axios";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { tokenManager } from "./tokenManager";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy function that forwards requests to the remote cloud server
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const proxyToRemote = async (
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction,
|
|
||||||
endpoint: string
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
|
|
||||||
|
|
||||||
logger.debug(`Proxying request to remote server: ${remoteUrl}`);
|
|
||||||
|
|
||||||
// Forward the request to the remote server
|
|
||||||
const response = await axios({
|
|
||||||
method: req.method as any,
|
|
||||||
url: remoteUrl,
|
|
||||||
data: req.body,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(await tokenManager.getAuthHeader()).headers
|
|
||||||
},
|
|
||||||
params: req.query,
|
|
||||||
timeout: 30000, // 30 second timeout
|
|
||||||
validateStatus: () => true // Don't throw on non-2xx status codes
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
|
|
||||||
|
|
||||||
// Forward the response status and data
|
|
||||||
return res.status(response.status).json(response.data);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error proxying request to remote server:", error);
|
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.SERVICE_UNAVAILABLE,
|
|
||||||
"Remote server is unavailable"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error.code === 'ECONNABORTED') {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.REQUEST_TIMEOUT,
|
|
||||||
"Request to remote server timed out"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error communicating with remote server"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
15
server/lib/resend.ts
Normal file
15
server/lib/resend.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export enum AudienceIds {
|
||||||
|
General = "",
|
||||||
|
Subscribed = "",
|
||||||
|
Churned = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let resend;
|
||||||
|
export default resend;
|
||||||
|
|
||||||
|
export async function moveEmailToAudience(
|
||||||
|
email: string,
|
||||||
|
audienceId: AudienceIds
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
5
server/lib/s3.ts
Normal file
5
server/lib/s3.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { S3Client } from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
|
export const s3Client = new S3Client({
|
||||||
|
region: process.env.S3_REGION || "us-east-1",
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import { APP_VERSION } from "./consts";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import license from "@server/license/license";
|
||||||
|
|
||||||
class TelemetryClient {
|
class TelemetryClient {
|
||||||
private client: PostHog | null = null;
|
private client: PostHog | null = null;
|
||||||
@@ -176,17 +177,36 @@ class TelemetryClient {
|
|||||||
|
|
||||||
const stats = await this.getSystemStats();
|
const stats = await this.getSystemStats();
|
||||||
|
|
||||||
this.client.capture({
|
if (build === "enterprise") {
|
||||||
distinctId: hostMeta.hostMetaId,
|
const licenseStatus = await license.check();
|
||||||
event: "supporter_status",
|
const payload = {
|
||||||
properties: {
|
distinctId: hostMeta.hostMetaId,
|
||||||
valid: stats.supporterStatus.valid,
|
event: "enterprise_status",
|
||||||
tier: stats.supporterStatus.tier,
|
properties: {
|
||||||
github_username: stats.supporterStatus.githubUsername
|
is_host_licensed: licenseStatus.isHostLicensed,
|
||||||
? this.anon(stats.supporterStatus.githubUsername)
|
is_license_valid: licenseStatus.isLicenseValid,
|
||||||
: "None"
|
license_tier: licenseStatus.tier || "unknown"
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
logger.debug("Sending enterprise startup telemtry payload:", {
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
// this.client.capture(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build === "oss") {
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: hostMeta.hostMetaId,
|
||||||
|
event: "supporter_status",
|
||||||
|
properties: {
|
||||||
|
valid: stats.supporterStatus.valid,
|
||||||
|
tier: stats.supporterStatus.tier,
|
||||||
|
github_username: stats.supporterStatus.githubUsername
|
||||||
|
? this.anon(stats.supporterStatus.githubUsername)
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.client.capture({
|
this.client.capture({
|
||||||
distinctId: hostMeta.hostMetaId,
|
distinctId: hostMeta.hostMetaId,
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export interface TokenResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
data: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token Manager - Handles automatic token refresh for hybrid server authentication
|
|
||||||
*
|
|
||||||
* Usage throughout the application:
|
|
||||||
* ```typescript
|
|
||||||
* import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
*
|
|
||||||
* // Get the current valid token
|
|
||||||
* const token = await tokenManager.getToken();
|
|
||||||
*
|
|
||||||
* // Force refresh if needed
|
|
||||||
* await tokenManager.refreshToken();
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The token manager automatically refreshes tokens every 24 hours by default
|
|
||||||
* and is started once in the privateHybridServer.ts file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class TokenManager {
|
|
||||||
private token: string | null = null;
|
|
||||||
private refreshInterval: NodeJS.Timeout | null = null;
|
|
||||||
private isRefreshing: boolean = false;
|
|
||||||
private refreshIntervalMs: number;
|
|
||||||
private retryInterval: NodeJS.Timeout | null = null;
|
|
||||||
private retryIntervalMs: number;
|
|
||||||
private tokenAvailablePromise: Promise<void> | null = null;
|
|
||||||
private tokenAvailableResolve: (() => void) | null = null;
|
|
||||||
|
|
||||||
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
|
|
||||||
// Default to 24 hours for refresh, 5 seconds for retry
|
|
||||||
this.refreshIntervalMs = refreshIntervalMs;
|
|
||||||
this.retryIntervalMs = retryIntervalMs;
|
|
||||||
this.setupTokenAvailablePromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up promise that resolves when token becomes available
|
|
||||||
*/
|
|
||||||
private setupTokenAvailablePromise(): void {
|
|
||||||
this.tokenAvailablePromise = new Promise((resolve) => {
|
|
||||||
this.tokenAvailableResolve = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the token available promise
|
|
||||||
*/
|
|
||||||
private resolveTokenAvailable(): void {
|
|
||||||
if (this.tokenAvailableResolve) {
|
|
||||||
this.tokenAvailableResolve();
|
|
||||||
this.tokenAvailableResolve = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the token manager - gets initial token and sets up refresh interval
|
|
||||||
* If initial token fetch fails, keeps retrying every few seconds until successful
|
|
||||||
*/
|
|
||||||
async start(): Promise<void> {
|
|
||||||
logger.info("Starting token manager...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.refreshToken();
|
|
||||||
this.setupRefreshInterval();
|
|
||||||
this.resolveTokenAvailable();
|
|
||||||
logger.info("Token manager started successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
|
|
||||||
this.setupRetryInterval();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up retry interval for initial token acquisition
|
|
||||||
*/
|
|
||||||
private setupRetryInterval(): void {
|
|
||||||
if (this.retryInterval) {
|
|
||||||
clearInterval(this.retryInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.retryInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
logger.debug("Retrying initial token acquisition");
|
|
||||||
await this.refreshToken();
|
|
||||||
this.setupRefreshInterval();
|
|
||||||
this.clearRetryInterval();
|
|
||||||
this.resolveTokenAvailable();
|
|
||||||
logger.info("Token manager started successfully after retry");
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug("Token acquisition retry failed, will try again");
|
|
||||||
}
|
|
||||||
}, this.retryIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear retry interval
|
|
||||||
*/
|
|
||||||
private clearRetryInterval(): void {
|
|
||||||
if (this.retryInterval) {
|
|
||||||
clearInterval(this.retryInterval);
|
|
||||||
this.retryInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the token manager and clear all intervals
|
|
||||||
*/
|
|
||||||
stop(): void {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
this.refreshInterval = null;
|
|
||||||
}
|
|
||||||
this.clearRetryInterval();
|
|
||||||
logger.info("Token manager stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current valid token
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
|
|
||||||
async getToken(): Promise<string> {
|
|
||||||
// If we don't have a token yet, wait for it to become available
|
|
||||||
if (!this.token && this.tokenAvailablePromise) {
|
|
||||||
await this.tokenAvailablePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.token) {
|
|
||||||
if (this.isRefreshing) {
|
|
||||||
// Wait for current refresh to complete
|
|
||||||
await this.waitForRefresh();
|
|
||||||
} else {
|
|
||||||
throw new Error("No valid token available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.token) {
|
|
||||||
throw new Error("No valid token available");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAuthHeader() {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await this.getToken()}`,
|
|
||||||
"X-CSRF-Token": "x-csrf-protection",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force refresh the token
|
|
||||||
*/
|
|
||||||
async refreshToken(): Promise<void> {
|
|
||||||
if (this.isRefreshing) {
|
|
||||||
await this.waitForRefresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRefreshing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const hybridConfig = config.getRawConfig().managed;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!hybridConfig?.id ||
|
|
||||||
!hybridConfig?.secret ||
|
|
||||||
!hybridConfig?.endpoint
|
|
||||||
) {
|
|
||||||
throw new Error("Hybrid configuration is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
|
|
||||||
|
|
||||||
const tokenData = {
|
|
||||||
remoteExitNodeId: hybridConfig.id,
|
|
||||||
secret: hybridConfig.secret
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Requesting new token from server");
|
|
||||||
|
|
||||||
const response = await axios.post<TokenResponse>(
|
|
||||||
tokenEndpoint,
|
|
||||||
tokenData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-CSRF-Token": "x-csrf-protection"
|
|
||||||
},
|
|
||||||
timeout: 10000 // 10 second timeout
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data.success) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get token: ${response.data.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.data.data.token) {
|
|
||||||
throw new Error("Received empty token from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.token = response.data.data.token;
|
|
||||||
logger.debug("Token refreshed successfully");
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating proxy mapping:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating proxy mapping:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Failed to refresh token");
|
|
||||||
} finally {
|
|
||||||
this.isRefreshing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up automatic token refresh interval
|
|
||||||
*/
|
|
||||||
private setupRefreshInterval(): void {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
logger.debug("Auto-refreshing token");
|
|
||||||
await this.refreshToken();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to auto-refresh token:", error);
|
|
||||||
}
|
|
||||||
}, this.refreshIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for current refresh operation to complete
|
|
||||||
*/
|
|
||||||
private async waitForRefresh(): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
if (!this.isRefreshing) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export a singleton instance for use throughout the application
|
|
||||||
export const tokenManager = new TokenManager();
|
|
||||||
@@ -6,14 +6,10 @@ import * as yaml from "js-yaml";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { db, exitNodes } from "@server/db";
|
import { db, exitNodes } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { tokenManager } from "../tokenManager";
|
|
||||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||||
import { getTraefikConfig } from "./";
|
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||||
import {
|
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||||
getValidCertificatesForDomains,
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
getValidCertificatesForDomainsHybrid
|
|
||||||
} from "../remoteCertificates";
|
|
||||||
import { sendToExitNode } from "../exitNodes";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export class TraefikConfigManager {
|
export class TraefikConfigManager {
|
||||||
@@ -313,93 +309,92 @@ export class TraefikConfigManager {
|
|||||||
this.lastActiveDomains = new Set(domains);
|
this.lastActiveDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan current local certificate state
|
if (
|
||||||
this.lastLocalCertificateState =
|
process.env.USE_PANGOLIN_DNS === "true" &&
|
||||||
await this.scanLocalCertificateState();
|
build != "oss"
|
||||||
|
) {
|
||||||
|
// Scan current local certificate state
|
||||||
|
this.lastLocalCertificateState =
|
||||||
|
await this.scanLocalCertificateState();
|
||||||
|
|
||||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||||
let validCertificates: Array<{
|
let validCertificates: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
wildcard: boolean | null;
|
wildcard: boolean | null;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (this.shouldFetchCertificates(domains)) {
|
if (this.shouldFetchCertificates(domains)) {
|
||||||
// Filter out domains that are already covered by wildcard certificates
|
// Filter out domains that are already covered by wildcard certificates
|
||||||
const domainsToFetch = new Set<string>();
|
const domainsToFetch = new Set<string>();
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
if (
|
if (
|
||||||
!isDomainCoveredByWildcard(
|
!isDomainCoveredByWildcard(
|
||||||
domain,
|
domain,
|
||||||
this.lastLocalCertificateState
|
this.lastLocalCertificateState
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
domainsToFetch.add(domain);
|
domainsToFetch.add(domain);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainsToFetch.size > 0) {
|
|
||||||
// Get valid certificates for domains not covered by wildcards
|
|
||||||
if (config.isManagedMode()) {
|
|
||||||
validCertificates =
|
|
||||||
await getValidCertificatesForDomainsHybrid(
|
|
||||||
domainsToFetch
|
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainsToFetch.size > 0) {
|
||||||
|
// Get valid certificates for domains not covered by wildcards
|
||||||
validCertificates =
|
validCertificates =
|
||||||
await getValidCertificatesForDomains(
|
await getValidCertificatesForDomains(
|
||||||
domainsToFetch
|
domainsToFetch
|
||||||
);
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download and decrypt new certificates
|
||||||
|
await this.processValidCertificates(validCertificates);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||||
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
this.lastCertificateFetch = new Date();
|
|
||||||
this.lastKnownDomains = new Set(domains);
|
|
||||||
|
|
||||||
logger.info(
|
// Always ensure all existing certificates (including wildcards) are in the config
|
||||||
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
);
|
|
||||||
|
|
||||||
// Download and decrypt new certificates
|
|
||||||
await this.processValidCertificates(validCertificates);
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
const timeSinceLastFetch = this.lastCertificateFetch
|
||||||
"All domains are covered by existing wildcard certificates, no fetch needed"
|
? Math.round(
|
||||||
);
|
(Date.now() -
|
||||||
this.lastCertificateFetch = new Date();
|
this.lastCertificateFetch.getTime()) /
|
||||||
this.lastKnownDomains = new Set(domains);
|
(1000 * 60)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// logger.debug(
|
||||||
|
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Still need to ensure config is up to date with existing certificates
|
||||||
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure all existing certificates (including wildcards) are in the config
|
// Clean up certificates for domains no longer in use
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
await this.cleanupUnusedCertificates(domains);
|
||||||
} else {
|
|
||||||
const timeSinceLastFetch = this.lastCertificateFetch
|
|
||||||
? Math.round(
|
|
||||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
|
||||||
(1000 * 60)
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// logger.debug(
|
// wait 1 second for traefik to pick up the new certificates
|
||||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
// );
|
|
||||||
|
|
||||||
// Still need to ensure config is up to date with existing certificates
|
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up certificates for domains no longer in use
|
|
||||||
await this.cleanupUnusedCertificates(domains);
|
|
||||||
|
|
||||||
// wait 1 second for traefik to pick up the new certificates
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Write traefik config as YAML to a second dynamic config file if changed
|
// Write traefik config as YAML to a second dynamic config file if changed
|
||||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||||
|
|
||||||
@@ -448,32 +443,15 @@ export class TraefikConfigManager {
|
|||||||
} | null> {
|
} | null> {
|
||||||
let traefikConfig;
|
let traefikConfig;
|
||||||
try {
|
try {
|
||||||
if (config.isManagedMode()) {
|
const currentExitNode = await getCurrentExitNodeId();
|
||||||
const resp = await axios.get(
|
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`,
|
traefikConfig = await getTraefikConfig(
|
||||||
await tokenManager.getAuthHeader()
|
// this is called by the local exit node to get its own config
|
||||||
);
|
currentExitNode,
|
||||||
|
config.getRawConfig().traefik.site_types,
|
||||||
if (resp.status !== 200) {
|
build == "oss", // filter out the namespace domains in open source
|
||||||
logger.error(
|
build != "oss" // generate the login pages on the cloud and hybrid
|
||||||
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`,
|
);
|
||||||
{ responseData: resp.data }
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
traefikConfig = resp.data.data;
|
|
||||||
} else {
|
|
||||||
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,
|
|
||||||
build == "oss", // filter out the namespace domains in open source
|
|
||||||
build != "oss" // generate the login pages on the cloud and hybrid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const domains = new Set<string>();
|
const domains = new Set<string>();
|
||||||
|
|
||||||
@@ -718,7 +696,12 @@ export class TraefikConfigManager {
|
|||||||
|
|
||||||
for (const cert of validCertificates) {
|
for (const cert of validCertificates) {
|
||||||
try {
|
try {
|
||||||
if (!cert.certFile || !cert.keyFile) {
|
if (
|
||||||
|
!cert.certFile ||
|
||||||
|
!cert.keyFile ||
|
||||||
|
cert.certFile.length === 0 ||
|
||||||
|
cert.keyFile.length === 0
|
||||||
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||||
);
|
);
|
||||||
@@ -842,7 +825,9 @@ export class TraefikConfigManager {
|
|||||||
const lastUpdateStr = fs
|
const lastUpdateStr = fs
|
||||||
.readFileSync(lastUpdatePath, "utf8")
|
.readFileSync(lastUpdatePath, "utf8")
|
||||||
.trim();
|
.trim();
|
||||||
lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000);
|
lastUpdateTime = Math.floor(
|
||||||
|
new Date(lastUpdateStr).getTime() / 1000
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
lastUpdateTime = null;
|
lastUpdateTime = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { db, exitNodes, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck } from "@server/db";
|
||||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
inArray,
|
||||||
|
or,
|
||||||
|
isNull,
|
||||||
|
ne,
|
||||||
|
isNotNull,
|
||||||
|
desc,
|
||||||
|
sql
|
||||||
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
import { resources, sites, Target, targets } from "@server/db";
|
||||||
import { build } from "@server/build";
|
|
||||||
import createPathRewriteMiddleware from "./middleware";
|
import createPathRewriteMiddleware from "./middleware";
|
||||||
import { sanitize, validatePathRewriteConfig } from "./utils";
|
import { sanitize, validatePathRewriteConfig } from "./utils";
|
||||||
|
|
||||||
@@ -79,7 +88,13 @@ export async function getTraefikConfig(
|
|||||||
and(
|
and(
|
||||||
eq(targets.enabled, true),
|
eq(targets.enabled, true),
|
||||||
eq(resources.enabled, true),
|
eq(resources.enabled, true),
|
||||||
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)),
|
or(
|
||||||
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
|
and(
|
||||||
|
isNull(sites.exitNodeId),
|
||||||
|
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes
|
||||||
|
)
|
||||||
|
),
|
||||||
or(
|
or(
|
||||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||||
@@ -105,7 +120,12 @@ export async function getTraefikConfig(
|
|||||||
const priority = row.priority ?? 100;
|
const priority = row.priority ?? 100;
|
||||||
|
|
||||||
// Create a unique key combining resourceId, path config, and rewrite config
|
// Create a unique key combining resourceId, path config, and rewrite config
|
||||||
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
|
const pathKey = [
|
||||||
|
targetPath,
|
||||||
|
pathMatchType,
|
||||||
|
rewritePath,
|
||||||
|
rewritePathType
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("-");
|
.join("-");
|
||||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||||
@@ -120,13 +140,15 @@ export async function getTraefikConfig(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
|
logger.error(
|
||||||
|
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resourcesMap.set(key, {
|
resourcesMap.set(key, {
|
||||||
resourceId: row.resourceId,
|
resourceId: row.resourceId,
|
||||||
name: resourceName,
|
name: resourceName,
|
||||||
fullDomain: row.fullDomain,
|
fullDomain: row.fullDomain,
|
||||||
ssl: row.ssl,
|
ssl: row.ssl,
|
||||||
http: row.http,
|
http: row.http,
|
||||||
@@ -158,9 +180,6 @@ export async function getTraefikConfig(
|
|||||||
port: row.port,
|
port: row.port,
|
||||||
internalPort: row.internalPort,
|
internalPort: row.internalPort,
|
||||||
enabled: row.targetEnabled,
|
enabled: row.targetEnabled,
|
||||||
rewritePath: row.rewritePath,
|
|
||||||
rewritePathType: row.rewritePathType,
|
|
||||||
priority: row.priority,
|
|
||||||
site: {
|
site: {
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
type: row.siteType,
|
type: row.siteType,
|
||||||
@@ -239,21 +258,18 @@ export async function getTraefikConfig(
|
|||||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tls = {};
|
const tls = {
|
||||||
if (build == "oss") {
|
certResolver: certResolver,
|
||||||
tls = {
|
...(preferWildcardCert
|
||||||
certResolver: certResolver,
|
? {
|
||||||
...(preferWildcardCert
|
domains: [
|
||||||
? {
|
{
|
||||||
domains: [
|
main: wildCard
|
||||||
{
|
}
|
||||||
main: wildCard
|
]
|
||||||
}
|
}
|
||||||
]
|
: {})
|
||||||
}
|
};
|
||||||
: {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
@@ -264,11 +280,12 @@ export async function getTraefikConfig(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Handle path rewriting middleware
|
// Handle path rewriting middleware
|
||||||
if (resource.rewritePath &&
|
if (
|
||||||
resource.path &&
|
resource.rewritePath !== null &&
|
||||||
|
resource.path !== null &&
|
||||||
resource.pathMatchType &&
|
resource.pathMatchType &&
|
||||||
resource.rewritePathType) {
|
resource.rewritePathType
|
||||||
|
) {
|
||||||
// Create a unique middleware name
|
// Create a unique middleware name
|
||||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
||||||
|
|
||||||
@@ -287,7 +304,10 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the middleware to the config
|
// the middleware to the config
|
||||||
Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
rewriteResult.middlewares
|
||||||
|
);
|
||||||
|
|
||||||
// middlewares to the router middleware chain
|
// middlewares to the router middleware chain
|
||||||
if (rewriteResult.chain) {
|
if (rewriteResult.chain) {
|
||||||
@@ -298,9 +318,13 @@ export async function getTraefikConfig(
|
|||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
|
logger.debug(
|
||||||
|
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
|
logger.error(
|
||||||
|
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +340,9 @@ export async function getTraefikConfig(
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
|
logger.warn(
|
||||||
|
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
headersArr.forEach((header) => {
|
headersArr.forEach((header) => {
|
||||||
@@ -482,14 +508,14 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
cookie: {
|
cookie: {
|
||||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||||
secure: resource.ssl,
|
secure: resource.ssl,
|
||||||
httpOnly: true
|
httpOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -590,13 +616,13 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
ipStrategy: {
|
ipStrategy: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
sourcePort: true
|
sourcePort: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1 @@
|
|||||||
import { build } from "@server/build";
|
export * from "./getTraefikConfig";
|
||||||
|
|
||||||
// Import both modules
|
|
||||||
import * as traefikModule from "./getTraefikConfig";
|
|
||||||
import * as privateTraefikModule from "./privateGetTraefikConfig";
|
|
||||||
|
|
||||||
// Conditionally export Traefik configuration implementation based on build type
|
|
||||||
const traefikImplementation = build === "oss" ? traefikModule : privateTraefikModule;
|
|
||||||
|
|
||||||
// Re-export all items from the selected implementation
|
|
||||||
export const { getTraefikConfig } = traefikImplementation;
|
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
import { db } from "@server/db";
|
import { db, hostMeta, HostMeta } from "@server/db";
|
||||||
import { hostMeta, licenseKey, sites } from "@server/db";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import NodeCache from "node-cache";
|
|
||||||
import { validateJWT } from "./licenseJwt";
|
|
||||||
import { count, eq } from "drizzle-orm";
|
|
||||||
import moment from "moment";
|
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
|
||||||
|
|
||||||
const keyTypes = ["HOST", "SITES"] as const;
|
const keyTypes = ["host"] as const;
|
||||||
type KeyType = (typeof keyTypes)[number];
|
export type LicenseKeyType = (typeof keyTypes)[number];
|
||||||
|
|
||||||
const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const;
|
const keyTiers = ["personal", "enterprise"] as const;
|
||||||
type KeyTier = (typeof keyTiers)[number];
|
export type LicenseKeyTier = (typeof keyTiers)[number];
|
||||||
|
|
||||||
export type LicenseStatus = {
|
export type LicenseStatus = {
|
||||||
isHostLicensed: boolean; // Are there any license keys?
|
isHostLicensed: boolean; // Are there any license keys?
|
||||||
isLicenseValid: boolean; // Is the license key valid?
|
isLicenseValid: boolean; // Is the license key valid?
|
||||||
hostId: string; // Host ID
|
hostId: string; // Host ID
|
||||||
maxSites?: number;
|
tier?: LicenseKeyTier;
|
||||||
usedSites?: number;
|
|
||||||
tier?: KeyTier;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LicenseKeyCache = {
|
export type LicenseKeyCache = {
|
||||||
@@ -28,451 +19,27 @@ export type LicenseKeyCache = {
|
|||||||
licenseKeyEncrypted: string;
|
licenseKeyEncrypted: string;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
iat?: Date;
|
iat?: Date;
|
||||||
type?: KeyType;
|
type?: LicenseKeyType;
|
||||||
tier?: KeyTier;
|
tier?: LicenseKeyTier;
|
||||||
numSites?: number;
|
terminateAt?: Date;
|
||||||
};
|
|
||||||
|
|
||||||
type ActivateLicenseKeyAPIResponse = {
|
|
||||||
data: {
|
|
||||||
instanceId: string;
|
|
||||||
};
|
|
||||||
success: boolean;
|
|
||||||
error: string;
|
|
||||||
message: string;
|
|
||||||
status: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValidateLicenseAPIResponse = {
|
|
||||||
data: {
|
|
||||||
licenseKeys: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
success: boolean;
|
|
||||||
error: string;
|
|
||||||
message: string;
|
|
||||||
status: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TokenPayload = {
|
|
||||||
valid: boolean;
|
|
||||||
type: KeyType;
|
|
||||||
tier: KeyTier;
|
|
||||||
quantity: number;
|
|
||||||
terminateAt: string; // ISO
|
|
||||||
iat: number; // Issued at
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class License {
|
export class License {
|
||||||
private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds
|
|
||||||
private validationServerUrl =
|
|
||||||
"https://api.fossorial.io/api/v1/license/professional/validate";
|
|
||||||
private activationServerUrl =
|
|
||||||
"https://api.fossorial.io/api/v1/license/professional/activate";
|
|
||||||
|
|
||||||
private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval });
|
|
||||||
private licenseKeyCache = new NodeCache();
|
|
||||||
|
|
||||||
private ephemeralKey!: string;
|
|
||||||
private statusKey = "status";
|
|
||||||
private serverSecret!: string;
|
private serverSecret!: string;
|
||||||
|
|
||||||
private publicKey = `-----BEGIN PUBLIC KEY-----
|
constructor(private hostMeta: HostMeta) {}
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
|
|
||||||
FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf
|
|
||||||
CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl
|
|
||||||
apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt
|
|
||||||
h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y
|
|
||||||
zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y
|
|
||||||
LQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----`;
|
|
||||||
|
|
||||||
constructor(private hostId: string) {
|
public async check(): Promise<LicenseStatus> {
|
||||||
this.ephemeralKey = Buffer.from(
|
return {
|
||||||
JSON.stringify({ ts: new Date().toISOString() })
|
hostId: this.hostMeta.hostMetaId,
|
||||||
).toString("base64");
|
isHostLicensed: false,
|
||||||
|
isLicenseValid: false
|
||||||
setInterval(
|
};
|
||||||
async () => {
|
|
||||||
await this.check();
|
|
||||||
},
|
|
||||||
1000 * 60 * 60
|
|
||||||
); // 1 hour = 60 * 60 = 3600 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
public listKeys(): LicenseKeyCache[] {
|
|
||||||
const keys = this.licenseKeyCache.keys();
|
|
||||||
return keys.map((key) => {
|
|
||||||
return this.licenseKeyCache.get<LicenseKeyCache>(key)!;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setServerSecret(secret: string) {
|
public setServerSecret(secret: string) {
|
||||||
this.serverSecret = secret;
|
this.serverSecret = secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async forceRecheck() {
|
|
||||||
this.statusCache.flushAll();
|
|
||||||
this.licenseKeyCache.flushAll();
|
|
||||||
|
|
||||||
return await this.check();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isUnlocked(): Promise<boolean> {
|
|
||||||
const status = await this.check();
|
|
||||||
if (status.isHostLicensed) {
|
|
||||||
if (status.isLicenseValid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async check(): Promise<LicenseStatus> {
|
|
||||||
// Set used sites
|
|
||||||
const [siteCount] = await db
|
|
||||||
.select({
|
|
||||||
value: count()
|
|
||||||
})
|
|
||||||
.from(sites);
|
|
||||||
|
|
||||||
const status: LicenseStatus = {
|
|
||||||
hostId: this.hostId,
|
|
||||||
isHostLicensed: true,
|
|
||||||
isLicenseValid: false,
|
|
||||||
maxSites: undefined,
|
|
||||||
usedSites: siteCount.value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.statusCache.has(this.statusKey)) {
|
|
||||||
const res = this.statusCache.get("status") as LicenseStatus;
|
|
||||||
res.usedSites = status.usedSites;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate all
|
|
||||||
this.licenseKeyCache.flushAll();
|
|
||||||
|
|
||||||
const allKeysRes = await db.select().from(licenseKey);
|
|
||||||
|
|
||||||
if (allKeysRes.length === 0) {
|
|
||||||
status.isHostLicensed = false;
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
let foundHostKey = false;
|
|
||||||
// Validate stored license keys
|
|
||||||
for (const key of allKeysRes) {
|
|
||||||
try {
|
|
||||||
// Decrypt the license key and token
|
|
||||||
const decryptedKey = decrypt(
|
|
||||||
key.licenseKeyId,
|
|
||||||
this.serverSecret
|
|
||||||
);
|
|
||||||
const decryptedToken = decrypt(
|
|
||||||
key.token,
|
|
||||||
this.serverSecret
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = validateJWT<TokenPayload>(
|
|
||||||
decryptedToken,
|
|
||||||
this.publicKey
|
|
||||||
);
|
|
||||||
|
|
||||||
this.licenseKeyCache.set<LicenseKeyCache>(decryptedKey, {
|
|
||||||
licenseKey: decryptedKey,
|
|
||||||
licenseKeyEncrypted: key.licenseKeyId,
|
|
||||||
valid: payload.valid,
|
|
||||||
type: payload.type,
|
|
||||||
tier: payload.tier,
|
|
||||||
numSites: payload.quantity,
|
|
||||||
iat: new Date(payload.iat * 1000)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (payload.type === "HOST") {
|
|
||||||
foundHostKey = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Error validating license key: ${key.licenseKeyId}`
|
|
||||||
);
|
|
||||||
logger.error(e);
|
|
||||||
|
|
||||||
this.licenseKeyCache.set<LicenseKeyCache>(
|
|
||||||
key.licenseKeyId,
|
|
||||||
{
|
|
||||||
licenseKey: key.licenseKeyId,
|
|
||||||
licenseKeyEncrypted: key.licenseKeyId,
|
|
||||||
valid: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHostKey && allKeysRes.length) {
|
|
||||||
logger.debug("No host license key found");
|
|
||||||
status.isHostLicensed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = allKeysRes.map((key) => ({
|
|
||||||
licenseKey: decrypt(key.licenseKeyId, this.serverSecret),
|
|
||||||
instanceId: decrypt(key.instanceId, this.serverSecret)
|
|
||||||
}));
|
|
||||||
|
|
||||||
let apiResponse: ValidateLicenseAPIResponse | undefined;
|
|
||||||
try {
|
|
||||||
// Phone home to validate license keys
|
|
||||||
apiResponse = await this.phoneHome(keys);
|
|
||||||
|
|
||||||
if (!apiResponse?.success) {
|
|
||||||
throw new Error(apiResponse?.error);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error communicating with license server:");
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Validate response", apiResponse);
|
|
||||||
|
|
||||||
// Check and update all license keys with server response
|
|
||||||
for (const key of keys) {
|
|
||||||
try {
|
|
||||||
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
|
|
||||||
key.licenseKey
|
|
||||||
)!;
|
|
||||||
const licenseKeyRes =
|
|
||||||
apiResponse?.data?.licenseKeys[key.licenseKey];
|
|
||||||
|
|
||||||
if (!apiResponse || !licenseKeyRes) {
|
|
||||||
logger.debug(
|
|
||||||
`No response from server for license key: ${key.licenseKey}`
|
|
||||||
);
|
|
||||||
if (cached.iat) {
|
|
||||||
const exp = moment(cached.iat)
|
|
||||||
.add(7, "days")
|
|
||||||
.toDate();
|
|
||||||
if (exp > new Date()) {
|
|
||||||
logger.debug(
|
|
||||||
`Using cached license key: ${key.licenseKey}, valid ${cached.valid}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Can't trust license key: ${key.licenseKey}`
|
|
||||||
);
|
|
||||||
cached.valid = false;
|
|
||||||
this.licenseKeyCache.set<LicenseKeyCache>(
|
|
||||||
key.licenseKey,
|
|
||||||
cached
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = validateJWT<TokenPayload>(
|
|
||||||
licenseKeyRes,
|
|
||||||
this.publicKey
|
|
||||||
);
|
|
||||||
cached.valid = payload.valid;
|
|
||||||
cached.type = payload.type;
|
|
||||||
cached.tier = payload.tier;
|
|
||||||
cached.numSites = payload.quantity;
|
|
||||||
cached.iat = new Date(payload.iat * 1000);
|
|
||||||
|
|
||||||
// Encrypt the updated token before storing
|
|
||||||
const encryptedKey = encrypt(
|
|
||||||
key.licenseKey,
|
|
||||||
this.serverSecret
|
|
||||||
);
|
|
||||||
const encryptedToken = encrypt(
|
|
||||||
licenseKeyRes,
|
|
||||||
this.serverSecret
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(licenseKey)
|
|
||||||
.set({
|
|
||||||
token: encryptedToken
|
|
||||||
})
|
|
||||||
.where(eq(licenseKey.licenseKeyId, encryptedKey));
|
|
||||||
|
|
||||||
this.licenseKeyCache.set<LicenseKeyCache>(
|
|
||||||
key.licenseKey,
|
|
||||||
cached
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Error validating license key: ${key}`);
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute host status
|
|
||||||
for (const key of keys) {
|
|
||||||
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
|
|
||||||
key.licenseKey
|
|
||||||
)!;
|
|
||||||
|
|
||||||
logger.debug("Checking key", cached);
|
|
||||||
|
|
||||||
if (cached.type === "HOST") {
|
|
||||||
status.isLicenseValid = cached.valid;
|
|
||||||
status.tier = cached.tier;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cached.valid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!status.maxSites) {
|
|
||||||
status.maxSites = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
status.maxSites += cached.numSites || 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking license status:");
|
|
||||||
logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.statusCache.set(this.statusKey, status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async activateLicenseKey(key: string) {
|
|
||||||
// Encrypt the license key before storing
|
|
||||||
const encryptedKey = encrypt(key, this.serverSecret);
|
|
||||||
|
|
||||||
const [existingKey] = await db
|
|
||||||
.select()
|
|
||||||
.from(licenseKey)
|
|
||||||
.where(eq(licenseKey.licenseKeyId, encryptedKey))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingKey) {
|
|
||||||
throw new Error("License key already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
let instanceId: string | undefined;
|
|
||||||
try {
|
|
||||||
// Call activate
|
|
||||||
const apiResponse = await fetch(this.activationServerUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
licenseKey: key,
|
|
||||||
instanceName: this.hostId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await apiResponse.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
throw new Error(`${data.message || data.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = data as ActivateLicenseKeyAPIResponse;
|
|
||||||
|
|
||||||
if (!response.data) {
|
|
||||||
throw new Error("No response from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.data.instanceId) {
|
|
||||||
throw new Error("No instance ID in response");
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceId = response.data.instanceId;
|
|
||||||
} catch (error) {
|
|
||||||
throw Error(`Error activating license key: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phone home to validate license key
|
|
||||||
const keys = [
|
|
||||||
{
|
|
||||||
licenseKey: key,
|
|
||||||
instanceId: instanceId!
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let validateResponse: ValidateLicenseAPIResponse;
|
|
||||||
try {
|
|
||||||
validateResponse = await this.phoneHome(keys);
|
|
||||||
|
|
||||||
if (!validateResponse) {
|
|
||||||
throw new Error("No response from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateResponse.success) {
|
|
||||||
throw new Error(validateResponse.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the license key
|
|
||||||
const licenseKeyRes = validateResponse.data.licenseKeys[key];
|
|
||||||
if (!licenseKeyRes) {
|
|
||||||
throw new Error("Invalid license key");
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = validateJWT<TokenPayload>(
|
|
||||||
licenseKeyRes,
|
|
||||||
this.publicKey
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!payload.valid) {
|
|
||||||
throw new Error("Invalid license key");
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedToken = encrypt(licenseKeyRes, this.serverSecret);
|
|
||||||
// Encrypt the instanceId before storing
|
|
||||||
const encryptedInstanceId = encrypt(instanceId!, this.serverSecret);
|
|
||||||
|
|
||||||
// Store the license key in the database
|
|
||||||
await db.insert(licenseKey).values({
|
|
||||||
licenseKeyId: encryptedKey,
|
|
||||||
token: encryptedToken,
|
|
||||||
instanceId: encryptedInstanceId
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw Error(`Error validating license key: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate the cache and re-compute the status
|
|
||||||
return await this.forceRecheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async phoneHome(
|
|
||||||
keys: {
|
|
||||||
licenseKey: string;
|
|
||||||
instanceId: string;
|
|
||||||
}[]
|
|
||||||
): Promise<ValidateLicenseAPIResponse> {
|
|
||||||
// Decrypt the instanceIds before sending to the server
|
|
||||||
const decryptedKeys = keys.map((key) => ({
|
|
||||||
licenseKey: key.licenseKey,
|
|
||||||
instanceId: key.instanceId
|
|
||||||
? decrypt(key.instanceId, this.serverSecret)
|
|
||||||
: key.instanceId
|
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await fetch(this.validationServerUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
licenseKeys: decryptedKeys,
|
|
||||||
ephemeralKey: this.ephemeralKey,
|
|
||||||
instanceName: this.hostId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return data as ValidateLicenseAPIResponse;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -483,6 +50,6 @@ if (!info) {
|
|||||||
throw new Error("Host information not found");
|
throw new Error("Host information not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const license = new License(info.hostMetaId);
|
export const license = new License(info);
|
||||||
|
|
||||||
export default license;
|
export default license;
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ export * from "./verifyIsLoggedInUser";
|
|||||||
export * from "./verifyIsLoggedInUser";
|
export * from "./verifyIsLoggedInUser";
|
||||||
export * from "./verifyClientAccess";
|
export * from "./verifyClientAccess";
|
||||||
export * from "./integration";
|
export * from "./integration";
|
||||||
export * from "./verifyValidLicense";
|
|
||||||
export * from "./verifyUserHasAction";
|
export * from "./verifyUserHasAction";
|
||||||
export * from "./verifyApiKeyAccess";
|
export * from "./verifyApiKeyAccess";
|
||||||
export * from "./verifyDomainAccess";
|
export * from "./verifyDomainAccess";
|
||||||
export * from "./verifyClientsEnabled";
|
export * from "./verifyClientsEnabled";
|
||||||
export * from "./verifyUserIsOrgOwner";
|
export * from "./verifyUserIsOrgOwner";
|
||||||
export * from "./verifySiteResourceAccess";
|
export * from "./verifySiteResourceAccess";
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export enum OpenAPITags {
|
|||||||
Invitation = "Invitation",
|
Invitation = "Invitation",
|
||||||
Target = "Target",
|
Target = "Target",
|
||||||
Rule = "Rule",
|
Rule = "Rule",
|
||||||
RuleTemplate = "Rule Template",
|
|
||||||
AccessToken = "Access Token",
|
AccessToken = "Access Token",
|
||||||
Idp = "Identity Provider",
|
Idp = "Identity Provider",
|
||||||
Client = "Client",
|
Client = "Client",
|
||||||
|
|||||||
28
server/private/cleanup.ts
Normal file
28
server/private/cleanup.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { rateLimitService } from "#private/lib/rateLimit";
|
||||||
|
import { cleanup as wsCleanup } from "#private/routers/ws";
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await rateLimitService.cleanup();
|
||||||
|
await wsCleanup();
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initCleanup() {
|
||||||
|
// Handle process termination
|
||||||
|
process.on("SIGTERM", () => cleanup());
|
||||||
|
process.on("SIGINT", () => cleanup());
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { customers, db } from "@server/db";
|
import { customers, db } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import stripe from "@server/lib/private/stripe";
|
import stripe from "#private/lib/stripe";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export async function createCustomer(
|
export async function createCustomer(
|
||||||
46
server/private/lib/billing/getOrgTierData.ts
Normal file
46
server/private/lib/billing/getOrgTierData.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getTierPriceSet } from "@server/lib/billing/tiers";
|
||||||
|
import { getOrgSubscriptionData } from "#private/routers/billing/getOrgSubscription";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
export async function getOrgTierData(
|
||||||
|
orgId: string
|
||||||
|
): Promise<{ tier: string | null; active: boolean }> {
|
||||||
|
let tier = null;
|
||||||
|
let active = false;
|
||||||
|
|
||||||
|
if (build !== "saas") {
|
||||||
|
return { tier, active };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { subscription, items } = await getOrgSubscriptionData(orgId);
|
||||||
|
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
const tierPriceSet = getTierPriceSet();
|
||||||
|
// Iterate through tiers in order (earlier keys are higher tiers)
|
||||||
|
for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
|
||||||
|
// Check if any subscription item matches this tier's price ID
|
||||||
|
const matchingItem = items.find((item) => item.priceId === priceId);
|
||||||
|
if (matchingItem) {
|
||||||
|
tier = tierId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subscription && subscription.status === "active") {
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
return { tier, active };
|
||||||
|
}
|
||||||
@@ -11,6 +11,5 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./limitSet";
|
export * from "./getOrgTierData";
|
||||||
export * from "./features";
|
export * from "./createCustomer";
|
||||||
export * from "./limitsService";
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user