mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 22:30:51 +00:00
Compare commits
213 Commits
policies
...
1.11.1-s.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bab6bb80 | ||
|
|
7536c03f63 | ||
|
|
ada5d2ef0e | ||
|
|
b8bead0590 | ||
|
|
68f852d6d1 | ||
|
|
d9fe5a8819 | ||
|
|
346183a23f | ||
|
|
dcfd7f5443 | ||
|
|
e59cd6672b | ||
|
|
7c8c440f67 | ||
|
|
f258c41f15 | ||
|
|
ae4a24f4aa | ||
|
|
476cdcfe86 | ||
|
|
f869df2f65 | ||
|
|
03cfabacd9 | ||
|
|
47ac5875f3 | ||
|
|
f67327358e | ||
|
|
4901823f15 | ||
|
|
5407e3c821 | ||
|
|
1d5cdad8b7 | ||
|
|
cd2424cb77 | ||
|
|
c17efde6bf | ||
|
|
40cd8cdec7 | ||
|
|
6768672a44 | ||
|
|
240c5b005b | ||
|
|
8dde170a35 | ||
|
|
c07abf8ff9 | ||
|
|
e5a436593f | ||
|
|
bb6e093ac6 | ||
|
|
59a334ce24 | ||
|
|
d241dcfb27 | ||
|
|
af263e7913 | ||
|
|
6610e7d405 | ||
|
|
c476e65cf2 | ||
|
|
b69b2eeeb3 | ||
|
|
89dab0917b | ||
|
|
73efdb95ae | ||
|
|
1bcca88614 | ||
|
|
3af1e0ef56 | ||
|
|
8387571c1d | ||
|
|
1d017f60b4 | ||
|
|
81effda9e8 | ||
|
|
9343906ab1 | ||
|
|
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
|
||||
.git
|
||||
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:
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: amd64-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,4 +47,6 @@ server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
*.mmdb
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
@@ -4,7 +4,7 @@ Contributions are welcome!
|
||||
|
||||
Please see the contribution and local development guide on the docs page before getting started:
|
||||
|
||||
https://docs.digpangolin.com/development/contributing
|
||||
https://docs.pangolin.net/development/contributing
|
||||
|
||||
### Licensing Considerations
|
||||
|
||||
|
||||
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 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
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
20
Makefile
20
Makefile
@@ -8,6 +8,7 @@ build-release:
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
@@ -16,6 +17,7 @@ build-release:
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
@@ -23,6 +25,24 @@ build-release:
|
||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-arm:
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
|
||||
149
README.md
149
README.md
@@ -1,157 +1,88 @@
|
||||
<div align="center">
|
||||
<h2>
|
||||
<picture>
|
||||
<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="250">
|
||||
<a href="https://pangolin.net/">
|
||||
<picture>
|
||||
<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>
|
||||
</a>
|
||||
</h2>
|
||||
</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">
|
||||
<h5>
|
||||
<a href="https://digpangolin.com">
|
||||
<a href="https://pangolin.net/">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
||||
Quick Install Guide
|
||||
<a href="https://docs.pangolin.net/">
|
||||
Documentation
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="mailto:contact@fossorial.io">
|
||||
<a href="mailto:contact@pangolin.net">
|
||||
Contact Us
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://digpangolin.com/slack">
|
||||
Slack
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://discord.gg/HCJR8Xhme4">
|
||||
Discord
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
[](https://digpangolin.com/slack)
|
||||
<div align="center">
|
||||
|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://pangolin.net/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://www.youtube.com/@fossorial-app)
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
|
||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
</strong>
|
||||
</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
|
||||
|
||||

|
||||
|
||||
## 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"/>
|
||||
Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
|
||||
## 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.pangolin.net/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.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
|
||||
Managed control plane, your infrastructure
|
||||
### Sign up and try now
|
||||
|
||||
- We manage database and control plane.
|
||||
- You self-host lightweight exit-node.
|
||||
- Traffic flows through your infra.
|
||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
||||
|
||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
||||
|
||||
### Full Enterprise On-Premises
|
||||
|
||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
||||
|
||||
## Project Development / Roadmap
|
||||
|
||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||
|
||||
## Contributions
|
||||
|
||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
||||
|
||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||
|
||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
||||
|
||||
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/`.
|
||||
@@ -3,7 +3,7 @@
|
||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||
|
||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
|
||||
- Description and location of the vulnerability.
|
||||
- Potential impact of the vulnerability.
|
||||
|
||||
@@ -8,7 +8,7 @@ import base64
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint'
|
||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||
HEADERS = {
|
||||
'accept': '*/*',
|
||||
'Authorization': 'Bearer <your_token_here>',
|
||||
|
||||
@@ -28,9 +28,9 @@ proxy-resources:
|
||||
# sso-roles:
|
||||
# - Member
|
||||
# sso-users:
|
||||
# - owen@fossorial.io
|
||||
# - owen@pangolin.net
|
||||
# whitelist-users:
|
||||
# - owen@fossorial.io
|
||||
# - owen@pangolin.net
|
||||
headers:
|
||||
- name: X-Example-Header
|
||||
value: example-value
|
||||
|
||||
@@ -12,7 +12,7 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "owen@fossorial.io",
|
||||
"email": "owen@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "milo@fossorial.io"
|
||||
"email": "milo@pangolin.net"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ put {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "numbat@fossorial.io",
|
||||
"email": "numbat@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: http://localhost:3002
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
import { build } from "@server/build";
|
||||
|
||||
let schema;
|
||||
if (build === "oss") {
|
||||
schema = [path.join("server", "db", "pg", "schema.ts")];
|
||||
} else {
|
||||
schema = [
|
||||
path.join("server", "db", "pg", "schema.ts"),
|
||||
path.join("server", "db", "pg", "privateSchema.ts")
|
||||
];
|
||||
}
|
||||
const schema = [
|
||||
path.join("server", "db", "pg", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { build } from "@server/build";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
let schema;
|
||||
if (build === "oss") {
|
||||
schema = [path.join("server", "db", "sqlite", "schema.ts")];
|
||||
} else {
|
||||
schema = [
|
||||
path.join("server", "db", "sqlite", "schema.ts"),
|
||||
path.join("server", "db", "sqlite", "privateSchema.ts")
|
||||
];
|
||||
}
|
||||
const schema = [
|
||||
path.join("server", "db", "sqlite", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
|
||||
210
esbuild.mjs
210
esbuild.mjs
@@ -2,8 +2,9 @@ import esbuild from "esbuild";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { nodeExternalsPlugin } from "esbuild-node-externals";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
// import { glob } from "glob";
|
||||
// import path from "path";
|
||||
|
||||
const banner = `
|
||||
// patch __dirname
|
||||
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
|
||||
`;
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage("Usage: $0 -entry [string] -out [string]")
|
||||
.usage("Usage: $0 -entry [string] -out [string] -build [string]")
|
||||
.option("entry", {
|
||||
alias: "e",
|
||||
describe: "Entry point file",
|
||||
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("build", {
|
||||
alias: "b",
|
||||
describe: "Build type (oss, saas, enterprise)",
|
||||
type: "string",
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: "oss",
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
|
||||
@@ -46,6 +54,179 @@ function getPackagePaths() {
|
||||
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
|
||||
.build({
|
||||
entryPoints: [argv.entry],
|
||||
@@ -59,6 +240,9 @@ esbuild
|
||||
platform: "node",
|
||||
external: ["body-parser"],
|
||||
plugins: [
|
||||
privateImportGuardPlugin(),
|
||||
dynamicImportGuardPlugin(),
|
||||
dynamicImportSwitcherPlugin(argv.build),
|
||||
nodeExternalsPlugin({
|
||||
packagePath: getPackagePaths(),
|
||||
}),
|
||||
@@ -66,7 +250,27 @@ esbuild
|
||||
sourcemap: "inline",
|
||||
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");
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
{{if .HybridMode}}
|
||||
managed:
|
||||
id: "{{.HybridId}}"
|
||||
secret: "{{.HybridSecret}}"
|
||||
|
||||
{{else}}
|
||||
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
@@ -28,6 +23,7 @@ server:
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||
{{if .EnableEmail}}
|
||||
email:
|
||||
smtp_host: "{{.EmailSMTPHost}}"
|
||||
@@ -41,4 +37,3 @@ flags:
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
{{end}}
|
||||
@@ -6,8 +6,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- pangolin-data:/var/certificates
|
||||
- pangolin-data:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
@@ -22,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
@@ -33,7 +31,7 @@ services:
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
- 21820:21820/udp
|
||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
||||
- 443:443
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
@@ -56,15 +54,9 @@ services:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||
# Shared volume for certificates and dynamic config in file mode
|
||||
- pangolin-data:/var/certificates:ro
|
||||
- pangolin-data:/var/dynamic:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
|
||||
volumes:
|
||||
pangolin-data:
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
@@ -3,17 +3,12 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
{{if not .HybridMode}}
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
{{else}}
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
{{end}}
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
@@ -27,7 +22,7 @@ log:
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
{{if not .HybridMode}}
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
@@ -36,22 +31,18 @@ certificatesResolvers:
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
{{end}}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
{{if .HybridMode}} proxyProtocol:
|
||||
trustedIPs:
|
||||
- 0.0.0.0/0
|
||||
- ::1/128{{end}}
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
{{if not .HybridMode}} http:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"{{end}}
|
||||
certResolver: "letsencrypt"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
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
|
||||
|
||||
require (
|
||||
golang.org/x/term v0.35.0
|
||||
golang.org/x/term v0.36.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.36.0 // indirect
|
||||
require golang.org/x/sys v0.37.0 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
195
install/main.go
195
install/main.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -48,10 +47,8 @@ type Config struct {
|
||||
InstallGerbil bool
|
||||
TraefikBouncerKey string
|
||||
DoCrowdsecInstall bool
|
||||
EnableGeoblocking bool
|
||||
Secret string
|
||||
HybridMode bool
|
||||
HybridId string
|
||||
HybridSecret string
|
||||
}
|
||||
|
||||
type SupportedContainer string
|
||||
@@ -98,24 +95,6 @@ func main() {
|
||||
|
||||
fmt.Println("\n=== Generating Configuration Files ===")
|
||||
|
||||
// If the secret and id are not generated then generate them
|
||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
||||
credentials, err := requestHybridCredentials()
|
||||
if err != nil {
|
||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
||||
os.Exit(1)
|
||||
}
|
||||
config.HybridId = credentials.RemoteExitNodeId
|
||||
config.HybridSecret = credentials.Secret
|
||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
||||
readBool(reader, "Have you adopted your node?", true)
|
||||
}
|
||||
|
||||
if err := createConfigFiles(config); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -125,6 +104,15 @@ func main() {
|
||||
|
||||
fmt.Println("\nConfiguration files created successfully!")
|
||||
|
||||
// Download MaxMind database if requested
|
||||
if config.EnableGeoblocking {
|
||||
fmt.Println("\n=== Downloading MaxMind Database ===")
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||
fmt.Println("You can download it manually later if needed.")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Starting installation ===")
|
||||
|
||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||
@@ -172,9 +160,34 @@ func main() {
|
||||
} else {
|
||||
alreadyInstalled = true
|
||||
fmt.Println("Looks like you already installed Pangolin!")
|
||||
|
||||
// Check if MaxMind database exists and offer to update it
|
||||
fmt.Println("\n=== MaxMind Database Update ===")
|
||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try updating it manually later if needed.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try downloading it manually later if needed.")
|
||||
}
|
||||
// Now you need to update your config file accordingly to enable geoblocking
|
||||
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
||||
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||
fmt.Println("Add the following line under the 'server' section:")
|
||||
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
||||
if !checkIsCrowdsecInstalledInCompose() {
|
||||
fmt.Println("\n=== CrowdSec Install ===")
|
||||
// check if crowdsec is installed
|
||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||
@@ -230,7 +243,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if !config.HybridMode && !alreadyInstalled {
|
||||
if !alreadyInstalled {
|
||||
// Setup Token Section
|
||||
fmt.Println("\n=== Setup Token ===")
|
||||
|
||||
@@ -251,9 +264,7 @@ func main() {
|
||||
|
||||
fmt.Println("\nInstallation complete!")
|
||||
|
||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
|
||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||
@@ -328,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
|
||||
// Basic configuration
|
||||
fmt.Println("\n=== Basic Configuration ===")
|
||||
for {
|
||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
||||
config.HybridMode = true
|
||||
break
|
||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
||||
config.HybridMode = false
|
||||
break
|
||||
}
|
||||
fmt.Println("Please answer 'yes' or 'no'")
|
||||
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
}
|
||||
|
||||
if config.HybridMode {
|
||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
||||
|
||||
if alreadyHaveCreds {
|
||||
config.HybridId = readString(reader, "Enter your ID", "")
|
||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
||||
}
|
||||
|
||||
// Try to get public IP as default
|
||||
publicIP := getPublicIP()
|
||||
if publicIP != "" {
|
||||
fmt.Printf("Detected public IP: %s\n", publicIP)
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
|
||||
config.InstallGerbil = true
|
||||
} else {
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Advanced configuration
|
||||
@@ -395,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
fmt.Println("\n=== Advanced Configuration ===")
|
||||
|
||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false)
|
||||
|
||||
if config.DashboardDomain == "" {
|
||||
fmt.Println("Error: Dashboard Domain name is required")
|
||||
@@ -429,11 +413,6 @@ func createConfigFiles(config Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// the hybrid does not need the dynamic config
|
||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip .DS_Store
|
||||
if strings.Contains(path, ".DS_Store") {
|
||||
return nil
|
||||
@@ -663,18 +642,30 @@ func checkPortsAvailable(port int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIsPangolinInstalledWithHybrid() bool {
|
||||
// Check if config/config.yml exists and contains hybrid section
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
return false
|
||||
func downloadMaxMindDatabase() error {
|
||||
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||
|
||||
// Download the GeoLite2 Country database
|
||||
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
|
||||
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
|
||||
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
|
||||
}
|
||||
|
||||
// Read config file to check for hybrid section
|
||||
content, err := os.ReadFile("config/config.yml")
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
// Extract the database
|
||||
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
|
||||
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
|
||||
}
|
||||
|
||||
// Check for hybrid section
|
||||
return bytes.Contains(content, []byte("managed:"))
|
||||
|
||||
// Find the .mmdb file and move it to the config directory
|
||||
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
|
||||
return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
|
||||
}
|
||||
|
||||
// Clean up the downloaded files
|
||||
if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
|
||||
fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
|
||||
"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": "Вижте всички сайтове",
|
||||
"siteTunnelDescription": "Определете как искате да се свържете с вашия сайт",
|
||||
"siteNewtCredentials": "Newt Удостоверения",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Създаден на",
|
||||
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
|
||||
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
|
||||
"proxyEnableSSL": "Активиране на SSL (https)",
|
||||
"proxyEnableSSL": "Активиране на SSL",
|
||||
"proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.",
|
||||
"target": "Цел",
|
||||
"configureTarget": "Конфигуриране на цели",
|
||||
"targetErrorFetch": "Неуспешно извличане на цели",
|
||||
"targetErrorFetchDescription": "Възникна грешка при извличане на целите",
|
||||
"siteErrorFetch": "Неуспешно извличане на ресурс",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Конфигурация на защитена връзка",
|
||||
"targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс",
|
||||
"targetTlsSettingsAdvanced": "Разширени TLS настройки",
|
||||
"targetTlsSni": "Име на TLS сървър (SNI)",
|
||||
"targetTlsSni": "Имя на TLS сървър",
|
||||
"targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.",
|
||||
"targetTlsSubmit": "Запазване на настройките",
|
||||
"targets": "Конфигурация на целите",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.",
|
||||
"methodSelect": "Изберете метод",
|
||||
"targetSubmit": "Добавяне на цел",
|
||||
"targetNoOne": "Няма цели. Добавете цел чрез формата.",
|
||||
"targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.",
|
||||
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
||||
"targetsSubmit": "Запазване на целите",
|
||||
"addTarget": "Добавете цел",
|
||||
"targetErrorInvalidIp": "Невалиден IP адрес",
|
||||
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
||||
"targetErrorInvalidPort": "Невалиден порт",
|
||||
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
|
||||
"targetErrorNoSite": "Няма избран сайт",
|
||||
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
|
||||
"targetCreated": "Целта е създадена",
|
||||
"targetCreatedDescription": "Целта беше успешно създадена",
|
||||
"targetErrorCreate": "Неуспешно създаване на целта",
|
||||
"targetErrorCreateDescription": "Възникна грешка при създаването на целта",
|
||||
"save": "Запази",
|
||||
"proxyAdditional": "Допълнителни настройки на прокси",
|
||||
"proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки",
|
||||
"proxyCustomHeader": "Персонализиран хост заглавие",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Администратор на сървър - Панголин",
|
||||
"licenseTierProfessional": "Професионален лиценз",
|
||||
"licenseTierEnterprise": "Предприятие лиценз",
|
||||
"licenseTierCommercial": "Търговски лиценз",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Лицензиран",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Име за показване за този доставчик на идентичност",
|
||||
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
|
||||
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
|
||||
"licenseBadge": "Професионален",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Тип доставчик",
|
||||
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
|
||||
"idpOidcConfigure": "Конфигурация на OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Навигационно меню",
|
||||
"navbarDescription": "Главно навигационно меню за приложението",
|
||||
"navbarDocsLink": "Документация",
|
||||
"commercialEdition": "Търговско издание",
|
||||
"otpErrorEnable": "Не може да се активира 2FA",
|
||||
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
|
||||
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Всички потребители",
|
||||
"sidebarIdentityProviders": "Идентификационни доставчици",
|
||||
"sidebarLicense": "Лиценз",
|
||||
"sidebarClients": "Клиенти (Бета)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Домейни",
|
||||
"enableDockerSocket": "Активиране на Docker Чернова",
|
||||
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Безплатен план",
|
||||
"billingWarningOverLimit": "Предупреждение: Превишили сте една или повече лимити за използване. Вашите сайтове няма да се свържат, докато не промените абонамента си или не коригирате използването.",
|
||||
"billingUsageLimitsOverview": "Преглед на лимитите за използване",
|
||||
"billingMonitorUsage": "Следете използването спрямо конфигурираните лимити. Ако ви е необходимо увеличаване на лимитите, моля, свържете се с нас на support@fossorial.io.",
|
||||
"billingMonitorUsage": "Следете използването спрямо конфигурираните лимити. Ако ви е необходимо увеличаване на лимитите, моля, свържете се с нас на support@pangolin.net.",
|
||||
"billingDataUsage": "Използване на данни",
|
||||
"billingOnlineTime": "Време на работа на сайта",
|
||||
"billingUsers": "Активни потребители",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.",
|
||||
"twoFactor": "Двуфакторно удостоверяване",
|
||||
"adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.",
|
||||
"continueToApplication": "Продължете към приложението",
|
||||
"securityKeyAdd": "Добавяне на ключ за сигурност",
|
||||
"securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност",
|
||||
"securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Външен прокси разрешен",
|
||||
"addNewTarget": "Добави нова цел",
|
||||
"targetsList": "Списък с цели",
|
||||
"advancedMode": "Разширен режим",
|
||||
"targetErrorDuplicateTargetFound": "Дублирана цел намерена",
|
||||
"healthCheckHealthy": "Здрав",
|
||||
"healthCheckUnhealthy": "Нездрав",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Грешка при автоматично влизане",
|
||||
"autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.",
|
||||
"autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Управление на самостоятелно хоствани",
|
||||
"remoteExitNodeDescription": "Управление на възли за разширяване на мрежовата ви свързаност",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Отдалечени възли",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Възли",
|
||||
"searchRemoteExitNodes": "Търсене на възли...",
|
||||
"remoteExitNodeAdd": "Добавяне на възел",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.",
|
||||
"remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)",
|
||||
"remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)",
|
||||
"sidebarRemoteExitNodes": "Възли (\"Local\" match)",
|
||||
"sidebarRemoteExitNodes": "Отдалечени възли",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Създаване на възел",
|
||||
"description": "Създайте нов възел, за да разширите мрежовата си свързаност",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Страницата за удостоверяване е актуализирана успешно",
|
||||
"healthCheckNotAvailable": "Локална",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||
"siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.",
|
||||
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Zobrazit všechny lokality",
|
||||
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
||||
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Vytvořeno v",
|
||||
"proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.",
|
||||
"proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.",
|
||||
"proxyEnableSSL": "Povolit SSL (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",
|
||||
"targetErrorFetchDescription": "Při načítání cílů došlo k chybě",
|
||||
"siteErrorFetch": "Nepodařilo se načíst zdroj",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Nastavení bezpečného připojení",
|
||||
"targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument",
|
||||
"targetTlsSettingsAdvanced": "Pokročilé nastavení TLS",
|
||||
"targetTlsSni": "Název serveru TLS (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í.",
|
||||
"targetTlsSubmit": "Uložit nastavení",
|
||||
"targets": "Konfigurace cílů",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
|
||||
"methodSelect": "Vyberte metodu",
|
||||
"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í.",
|
||||
"targetsSubmit": "Uložit cíle",
|
||||
"addTarget": "Add Target",
|
||||
"targetErrorInvalidIp": "Neplatná IP adresa",
|
||||
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
||||
"targetErrorInvalidPort": "Neplatný port",
|
||||
"targetErrorInvalidPortDescription": "Zadejte platné číslo portu",
|
||||
"targetErrorNoSite": "Není vybrán žádný web",
|
||||
"targetErrorNoSiteDescription": "Vyberte prosím web pro cíl",
|
||||
"targetCreated": "Cíl byl vytvořen",
|
||||
"targetCreatedDescription": "Cíl byl úspěšně vytvořen",
|
||||
"targetErrorCreate": "Nepodařilo se vytvořit cíl",
|
||||
"targetErrorCreateDescription": "Došlo k chybě při vytváření cíle",
|
||||
"save": "Uložit",
|
||||
"proxyAdditional": "Další nastavení proxy",
|
||||
"proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje",
|
||||
"proxyCustomHeader": "Vlastní hlavička hostitele",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Správce serveru - Pangolin",
|
||||
"licenseTierProfessional": "Profesionální licence",
|
||||
"licenseTierEnterprise": "Podniková licence",
|
||||
"licenseTierCommercial": "Obchodní licence",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Licencováno",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
|
||||
"idpAutoProvisionUsers": "Automatická úprava uživatelů",
|
||||
"idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.",
|
||||
"licenseBadge": "Profesionální",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Typ poskytovatele",
|
||||
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
|
||||
"idpOidcConfigure": "Nastavení OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Navigation Menu",
|
||||
"navbarDescription": "Hlavní navigační menu aplikace",
|
||||
"navbarDocsLink": "Dokumentace",
|
||||
"commercialEdition": "Obchodní vydání",
|
||||
"otpErrorEnable": "2FA nelze povolit",
|
||||
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
|
||||
"otpSetupCheckCode": "Zadejte 6místný kód",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Všichni uživatelé",
|
||||
"sidebarIdentityProviders": "Poskytovatelé identity",
|
||||
"sidebarLicense": "Licence",
|
||||
"sidebarClients": "Klienti (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domény",
|
||||
"enableDockerSocket": "Povolit Docker plán",
|
||||
"enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Volná úroveň",
|
||||
"billingWarningOverLimit": "Upozornění: Překročili jste jeden nebo více omezení používání. Vaše stránky se nepřipojí dokud nezměníte předplatné nebo neupravíte své používání.",
|
||||
"billingUsageLimitsOverview": "Přehled omezení použití",
|
||||
"billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@fossorial.io.",
|
||||
"billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@pangolin.net.",
|
||||
"billingDataUsage": "Využití dat",
|
||||
"billingOnlineTime": "Stránka online čas",
|
||||
"billingUsers": "Aktivní uživatelé",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.",
|
||||
"twoFactor": "Dvoufaktorové ověření",
|
||||
"adminEnabled2FaOnYourAccount": "Váš správce povolil dvoufaktorové ověřování pro {email}. Chcete-li pokračovat, dokončete proces nastavení.",
|
||||
"continueToApplication": "Pokračovat v aplikaci",
|
||||
"securityKeyAdd": "Přidat bezpečnostní klíč",
|
||||
"securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč",
|
||||
"securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Externí proxy povolen",
|
||||
"addNewTarget": "Add New Target",
|
||||
"targetsList": "Seznam cílů",
|
||||
"advancedMode": "Pokročilý režim",
|
||||
"targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl",
|
||||
"healthCheckHealthy": "Zdravé",
|
||||
"healthCheckUnhealthy": "Nezdravé",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Automatická chyba přihlášení",
|
||||
"autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.",
|
||||
"autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Spravovat vlastní hostování",
|
||||
"remoteExitNodeDescription": "Spravujte uzly pro rozšíření připojení k síti",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Vzdálené uzly",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Uzly",
|
||||
"searchRemoteExitNodes": "Hledat uzly...",
|
||||
"remoteExitNodeAdd": "Přidat uzel",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.",
|
||||
"remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu",
|
||||
"remoteExitNodeDelete": "Odstranit uzel",
|
||||
"sidebarRemoteExitNodes": "Uzly",
|
||||
"sidebarRemoteExitNodes": "Vzdálené uzly",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Vytvořit uzel",
|
||||
"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",
|
||||
"healthCheckNotAvailable": "Místní",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
||||
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Alle Standorte anzeigen",
|
||||
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
||||
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Erstellt am",
|
||||
"proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.",
|
||||
"proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.",
|
||||
"proxyEnableSSL": "SSL aktivieren (https)",
|
||||
"proxyEnableSSL": "SSL aktivieren",
|
||||
"proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Ziele konfigurieren",
|
||||
"targetErrorFetch": "Fehler beim Abrufen der Ziele",
|
||||
"targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten",
|
||||
"siteErrorFetch": "Fehler beim Abrufen der Ressource",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Sicherheitskonfiguration",
|
||||
"targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource",
|
||||
"targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen",
|
||||
"targetTlsSni": "TLS-Servername (SNI)",
|
||||
"targetTlsSni": "TLS Servername",
|
||||
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
||||
"targetTlsSubmit": "Einstellungen speichern",
|
||||
"targets": "Ziel-Konfiguration",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
||||
"methodSelect": "Methode auswählen",
|
||||
"targetSubmit": "Ziel hinzufügen",
|
||||
"targetNoOne": "Keine Ziele. Fügen Sie ein Ziel über das Formular hinzu.",
|
||||
"targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.",
|
||||
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
||||
"targetsSubmit": "Ziele speichern",
|
||||
"addTarget": "Ziel hinzufügen",
|
||||
"targetErrorInvalidIp": "Ungültige IP-Adresse",
|
||||
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
|
||||
"targetErrorInvalidPort": "Ungültiger Port",
|
||||
"targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein",
|
||||
"targetErrorNoSite": "Keine Site ausgewählt",
|
||||
"targetErrorNoSiteDescription": "Bitte wähle eine Seite für das Ziel aus",
|
||||
"targetCreated": "Ziel erstellt",
|
||||
"targetCreatedDescription": "Ziel wurde erfolgreich erstellt",
|
||||
"targetErrorCreate": "Fehler beim Erstellen des Ziels",
|
||||
"targetErrorCreateDescription": "Beim Erstellen des Ziels ist ein Fehler aufgetreten",
|
||||
"save": "Speichern",
|
||||
"proxyAdditional": "Zusätzliche Proxy-Einstellungen",
|
||||
"proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht",
|
||||
"proxyCustomHeader": "Benutzerdefinierter Host-Header",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||
"licenseTierProfessional": "Professional Lizenz",
|
||||
"licenseTierEnterprise": "Enterprise Lizenz",
|
||||
"licenseTierCommercial": "Gewerbliche Lizenz",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Lizenziert",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
|
||||
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
|
||||
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
|
||||
"licenseBadge": "Profi",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Anbietertyp",
|
||||
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Konfiguration",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Navigationsmenü",
|
||||
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
|
||||
"navbarDocsLink": "Dokumentation",
|
||||
"commercialEdition": "Kommerzielle Edition",
|
||||
"otpErrorEnable": "2FA konnte nicht aktiviert werden",
|
||||
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
|
||||
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Alle Benutzer",
|
||||
"sidebarIdentityProviders": "Identitätsanbieter",
|
||||
"sidebarLicense": "Lizenz",
|
||||
"sidebarClients": "Kunden (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domänen",
|
||||
"enableDockerSocket": "Docker Blaupause aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Kostenlose Stufe",
|
||||
"billingWarningOverLimit": "Warnung: Sie haben ein oder mehrere Nutzungslimits überschritten. Ihre Webseiten werden nicht verbunden, bis Sie Ihr Abonnement ändern oder Ihren Verbrauch anpassen.",
|
||||
"billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen",
|
||||
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@fossorial.io.",
|
||||
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.",
|
||||
"billingDataUsage": "Datenverbrauch",
|
||||
"billingOnlineTime": "Online-Zeit der Seite",
|
||||
"billingUsers": "Aktive Benutzer",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.",
|
||||
"twoFactor": "Zwei-Faktor-Authentifizierung",
|
||||
"adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.",
|
||||
"continueToApplication": "Weiter zur Anwendung",
|
||||
"securityKeyAdd": "Sicherheitsschlüssel hinzufügen",
|
||||
"securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren",
|
||||
"securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Externer Proxy aktiviert",
|
||||
"addNewTarget": "Neues Ziel hinzufügen",
|
||||
"targetsList": "Ziel-Liste",
|
||||
"advancedMode": "Erweiterter Modus",
|
||||
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
|
||||
"healthCheckHealthy": "Gesund",
|
||||
"healthCheckUnhealthy": "Ungesund",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
||||
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
||||
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Selbst-Hosted verwalten",
|
||||
"remoteExitNodeDescription": "Knoten verwalten, um die Netzwerkverbindung zu erweitern",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Knoten",
|
||||
"searchRemoteExitNodes": "Knoten suchen...",
|
||||
"remoteExitNodeAdd": "Knoten hinzufügen",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.",
|
||||
"remoteExitNodeConfirmDelete": "Löschknoten bestätigen",
|
||||
"remoteExitNodeDelete": "Knoten löschen",
|
||||
"sidebarRemoteExitNodes": "Knoten",
|
||||
"sidebarRemoteExitNodes": "Entfernte Knoten",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Knoten erstellen",
|
||||
"description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Auth-Seite erfolgreich aktualisiert",
|
||||
"healthCheckNotAvailable": "Lokal",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"setupNewOrg": "New Organization",
|
||||
@@ -48,9 +47,8 @@
|
||||
"edit": "Edit",
|
||||
"siteConfirmDelete": "Confirm Delete Site",
|
||||
"siteDelete": "Delete Site",
|
||||
"siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.",
|
||||
"siteMessageConfirm": "To confirm, please type the name of the site below.",
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?",
|
||||
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Allow connectivity to your network through secure tunnels",
|
||||
"siteCreate": "Create Site",
|
||||
@@ -97,7 +95,7 @@
|
||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "See All Sites",
|
||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||
"siteNewtCredentials": "Newt Credentials",
|
||||
@@ -155,8 +153,7 @@
|
||||
"protected": "Protected",
|
||||
"notProtected": "Not Protected",
|
||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||
"resourceMessageConfirm": "To confirm, please type the name of the resource below.",
|
||||
"resourceQuestionRemove": "Are you sure you want to remove the resource {selectedResource} from the organization?",
|
||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||
"resourceHTTP": "HTTPS Resource",
|
||||
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
@@ -221,7 +218,7 @@
|
||||
"orgDeleteConfirm": "Confirm Delete Organization",
|
||||
"orgMessageRemove": "This action is irreversible and will delete all associated data.",
|
||||
"orgMessageConfirm": "To confirm, please type the name of the organization below.",
|
||||
"orgQuestionRemove": "Are you sure you want to remove the organization {selectedOrg}?",
|
||||
"orgQuestionRemove": "Are you sure you want to remove the organization?",
|
||||
"orgUpdated": "Organization updated",
|
||||
"orgUpdatedDescription": "The organization has been updated.",
|
||||
"orgErrorUpdate": "Failed to update organization",
|
||||
@@ -288,9 +285,8 @@
|
||||
"apiKeysAdd": "Generate API Key",
|
||||
"apiKeysErrorDelete": "Error deleting API key",
|
||||
"apiKeysErrorDeleteMessage": "Error deleting API key",
|
||||
"apiKeysQuestionRemove": "Are you sure you want to remove the API key {selectedApiKey} from the organization?",
|
||||
"apiKeysQuestionRemove": "Are you sure you want to remove the API key from the organization?",
|
||||
"apiKeysMessageRemove": "Once removed, the API key will no longer be able to be used.",
|
||||
"apiKeysMessageConfirm": "To confirm, please type the name of the API key below.",
|
||||
"apiKeysDeleteConfirm": "Confirm Delete API Key",
|
||||
"apiKeysDelete": "Delete API Key",
|
||||
"apiKeysManage": "Manage API Keys",
|
||||
@@ -306,8 +302,7 @@
|
||||
"userDeleteConfirm": "Confirm Delete User",
|
||||
"userDeleteServer": "Delete User from Server",
|
||||
"userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.",
|
||||
"userMessageConfirm": "To confirm, please type the name of the user below.",
|
||||
"userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?",
|
||||
"userQuestionRemove": "Are you sure you want to permanently delete user from the server?",
|
||||
"licenseKey": "License Key",
|
||||
"valid": "Valid",
|
||||
"numberOfSites": "Number of Sites",
|
||||
@@ -340,7 +335,7 @@
|
||||
"fossorialLicense": "View Fossorial Commercial License & Subscription Terms",
|
||||
"licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.",
|
||||
"licenseMessageConfirm": "To confirm, please type the license key below.",
|
||||
"licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?",
|
||||
"licenseQuestionRemove": "Are you sure you want to delete the license key ?",
|
||||
"licenseKeyDelete": "Delete License Key",
|
||||
"licenseKeyDeleteConfirm": "Confirm Delete License Key",
|
||||
"licenseTitle": "Manage License Status",
|
||||
@@ -373,7 +368,7 @@
|
||||
"inviteRemoveErrorDescription": "An error occurred while removing the invitation.",
|
||||
"inviteRemoved": "Invitation removed",
|
||||
"inviteRemovedDescription": "The invitation for {email} has been removed.",
|
||||
"inviteQuestionRemove": "Are you sure you want to remove the invitation {email}?",
|
||||
"inviteQuestionRemove": "Are you sure you want to remove the invitation?",
|
||||
"inviteMessageRemove": "Once removed, this invitation will no longer be valid. You can always re-invite the user later.",
|
||||
"inviteMessageConfirm": "To confirm, please type the email address of the invitation below.",
|
||||
"inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for {email}? This will revoke the previous invitation.",
|
||||
@@ -399,9 +394,8 @@
|
||||
"userErrorOrgRemoveDescription": "An error occurred while removing the user.",
|
||||
"userOrgRemoved": "User removed",
|
||||
"userOrgRemovedDescription": "The user {email} has been removed from the organization.",
|
||||
"userQuestionOrgRemove": "Are you sure you want to remove {email} from the organization?",
|
||||
"userQuestionOrgRemove": "Are you sure you want to remove this user from the organization?",
|
||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||
"userMessageOrgConfirm": "To confirm, please type the name of the of the user below.",
|
||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||
"userRemoveOrg": "Remove User from Organization",
|
||||
"users": "Users",
|
||||
@@ -469,7 +463,10 @@
|
||||
"createdAt": "Created At",
|
||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
||||
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||
"proxyEnableSSL": "Enable SSL (https)",
|
||||
"proxyEnableSSL": "Enable SSL",
|
||||
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configure Targets",
|
||||
"targetErrorFetch": "Failed to fetch targets",
|
||||
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
||||
"siteErrorFetch": "Failed to fetch resource",
|
||||
@@ -496,7 +493,7 @@
|
||||
"targetTlsSettings": "Secure Connection Configuration",
|
||||
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
||||
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
||||
"targetTlsSni": "TLS Server Name (SNI)",
|
||||
"targetTlsSni": "TLS Server Name",
|
||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||
"targetTlsSubmit": "Save Settings",
|
||||
"targets": "Targets Configuration",
|
||||
@@ -505,9 +502,21 @@
|
||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||
"methodSelect": "Select method",
|
||||
"targetSubmit": "Add Target",
|
||||
"targetNoOne": "No targets. Add a target using the form.",
|
||||
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
|
||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||
"targetsSubmit": "Save Targets",
|
||||
"addTarget": "Add Target",
|
||||
"targetErrorInvalidIp": "Invalid IP address",
|
||||
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
||||
"targetErrorInvalidPort": "Invalid port",
|
||||
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
||||
"targetErrorNoSite": "No site selected",
|
||||
"targetErrorNoSiteDescription": "Please select a site for the target",
|
||||
"targetCreated": "Target created",
|
||||
"targetCreatedDescription": "Target has been created successfully",
|
||||
"targetErrorCreate": "Failed to create target",
|
||||
"targetErrorCreateDescription": "An error occurred while creating the target",
|
||||
"save": "Save",
|
||||
"proxyAdditional": "Additional Proxy Settings",
|
||||
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
||||
"proxyCustomHeader": "Custom Host Header",
|
||||
@@ -716,7 +725,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
"licenseTierProfessional": "Professional License",
|
||||
"licenseTierEnterprise": "Enterprise License",
|
||||
"licenseTierCommercial": "Commercial License",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Licensed",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
@@ -728,7 +737,7 @@
|
||||
"idpManageDescription": "View and manage identity providers in the system",
|
||||
"idpDeletedDescription": "Identity provider deleted successfully",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?",
|
||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
||||
"idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.",
|
||||
"idpMessageConfirm": "To confirm, please type the name of the identity provider below.",
|
||||
"idpConfirmDelete": "Confirm Delete Identity Provider",
|
||||
@@ -751,7 +760,7 @@
|
||||
"idpDisplayName": "A display name for this identity provider",
|
||||
"idpAutoProvisionUsers": "Auto Provision Users",
|
||||
"idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.",
|
||||
"licenseBadge": "Professional",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Provider Type",
|
||||
"idpTypeDescription": "Select the type of identity provider you want to configure",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
||||
@@ -1041,26 +1050,6 @@
|
||||
"actionDeleteResourceRule": "Delete Resource Rule",
|
||||
"actionListResourceRules": "List Resource Rules",
|
||||
"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",
|
||||
"actionCheckOrgId": "Check ID",
|
||||
"actionCreateOrg": "Create Organization",
|
||||
@@ -1105,7 +1094,6 @@
|
||||
"navbar": "Navigation Menu",
|
||||
"navbarDescription": "Main navigation menu for the application",
|
||||
"navbarDocsLink": "Documentation",
|
||||
"commercialEdition": "Commercial Edition",
|
||||
"otpErrorEnable": "Unable to enable 2FA",
|
||||
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
|
||||
"otpSetupCheckCode": "Please enter a 6-digit code",
|
||||
@@ -1156,13 +1144,12 @@
|
||||
"sidebarInvitations": "Invitations",
|
||||
"sidebarRoles": "Roles",
|
||||
"sidebarShareableLinks": "Shareable Links",
|
||||
"sidebarRuleTemplates": "Rule Templates",
|
||||
"sidebarApiKeys": "API Keys",
|
||||
"sidebarSettings": "Settings",
|
||||
"sidebarAllUsers": "All Users",
|
||||
"sidebarIdentityProviders": "Identity Providers",
|
||||
"sidebarLicense": "License",
|
||||
"sidebarClients": "Clients (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domains",
|
||||
"enableDockerSocket": "Enable Docker Blueprint",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||
@@ -1219,9 +1206,8 @@
|
||||
"domainCreate": "Create Domain",
|
||||
"domainCreatedDescription": "Domain created successfully",
|
||||
"domainDeletedDescription": "Domain deleted successfully",
|
||||
"domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?",
|
||||
"domainQuestionRemove": "Are you sure you want to remove the domain from your account?",
|
||||
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
|
||||
"domainMessageConfirm": "To confirm, please type the domain name below.",
|
||||
"domainConfirmDelete": "Confirm Delete Domain",
|
||||
"domainDelete": "Delete Domain",
|
||||
"domain": "Domain",
|
||||
@@ -1288,7 +1274,7 @@
|
||||
"billingFreeTier": "Free Tier",
|
||||
"billingWarningOverLimit": "Warning: You have exceeded one or more usage limits. Your sites will not connect until you modify your subscription or adjust your usage.",
|
||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||
"billingDataUsage": "Data Usage",
|
||||
"billingOnlineTime": "Site Online Time",
|
||||
"billingUsers": "Active Users",
|
||||
@@ -1355,7 +1341,6 @@
|
||||
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
||||
"twoFactor": "Two-Factor Authentication",
|
||||
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
||||
"continueToApplication": "Continue to Application",
|
||||
"securityKeyAdd": "Add Security Key",
|
||||
"securityKeyRegisterTitle": "Register New Security Key",
|
||||
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
|
||||
@@ -1433,6 +1418,7 @@
|
||||
"externalProxyEnabled": "External Proxy Enabled",
|
||||
"addNewTarget": "Add New Target",
|
||||
"targetsList": "Targets List",
|
||||
"advancedMode": "Advanced Mode",
|
||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||
"healthCheckHealthy": "Healthy",
|
||||
"healthCheckUnhealthy": "Unhealthy",
|
||||
@@ -1565,18 +1551,17 @@
|
||||
"autoLoginError": "Auto Login Error",
|
||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted",
|
||||
"remoteExitNodeDescription": "Manage nodes to extend your network connectivity",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nodes",
|
||||
"searchRemoteExitNodes": "Search nodes...",
|
||||
"remoteExitNodeAdd": "Add Node",
|
||||
"remoteExitNodeErrorDelete": "Error deleting node",
|
||||
"remoteExitNodeQuestionRemove": "Are you sure you want to remove the node {selectedNode} from the organization?",
|
||||
"remoteExitNodeQuestionRemove": "Are you sure you want to remove the node from the organization?",
|
||||
"remoteExitNodeMessageRemove": "Once removed, the node will no longer be accessible.",
|
||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||
"remoteExitNodeDelete": "Delete Node",
|
||||
"sidebarRemoteExitNodes": "Nodes",
|
||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Create Node",
|
||||
"description": "Create a new node to extend your network connectivity",
|
||||
@@ -1746,5 +1731,165 @@
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"rewritePath": "Rewrite Path",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"cannotbeUndone": "This can not be undone.",
|
||||
"toConfirm": "to confirm",
|
||||
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site."
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
||||
"siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||
"siteLocalDescription": "Solo recursos locales. Sin túneles.",
|
||||
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Ver todos los sitios",
|
||||
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
||||
"siteNewtCredentials": "Credenciales nuevas",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Creado el",
|
||||
"proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.",
|
||||
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
|
||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
||||
"proxyEnableSSL": "Activar SSL",
|
||||
"proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurar objetivos",
|
||||
"targetErrorFetch": "Error al recuperar los objetivos",
|
||||
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
|
||||
"siteErrorFetch": "No se pudo obtener el recurso",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Configuración de conexión segura",
|
||||
"targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso",
|
||||
"targetTlsSettingsAdvanced": "Ajustes avanzados de TLS",
|
||||
"targetTlsSni": "Nombre del servidor TLS (SNI)",
|
||||
"targetTlsSni": "Nombre del servidor TLS",
|
||||
"targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.",
|
||||
"targetTlsSubmit": "Guardar ajustes",
|
||||
"targets": "Configuración de objetivos",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.",
|
||||
"methodSelect": "Seleccionar método",
|
||||
"targetSubmit": "Añadir destino",
|
||||
"targetNoOne": "No hay objetivos. Agregue un objetivo usando el formulario.",
|
||||
"targetNoOne": "Este recurso no tiene ningún objetivo. Agrega un objetivo para configurar dónde enviar peticiones al backend.",
|
||||
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
||||
"targetsSubmit": "Guardar objetivos",
|
||||
"addTarget": "Añadir destino",
|
||||
"targetErrorInvalidIp": "Dirección IP inválida",
|
||||
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
|
||||
"targetErrorInvalidPort": "Puerto inválido",
|
||||
"targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido",
|
||||
"targetErrorNoSite": "Ningún sitio seleccionado",
|
||||
"targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo",
|
||||
"targetCreated": "Objetivo creado",
|
||||
"targetCreatedDescription": "El objetivo se ha creado correctamente",
|
||||
"targetErrorCreate": "Error al crear el objetivo",
|
||||
"targetErrorCreateDescription": "Se ha producido un error al crear el objetivo",
|
||||
"save": "Guardar",
|
||||
"proxyAdditional": "Ajustes adicionales del proxy",
|
||||
"proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy",
|
||||
"proxyCustomHeader": "Cabecera de host personalizada",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Admin Servidor - Pangolin",
|
||||
"licenseTierProfessional": "Licencia profesional",
|
||||
"licenseTierEnterprise": "Licencia Enterprise",
|
||||
"licenseTierCommercial": "Licencia comercial",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Licenciado",
|
||||
"yes": "Sí",
|
||||
"no": "Nu",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
|
||||
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios",
|
||||
"idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.",
|
||||
"licenseBadge": "Profesional",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo de proveedor",
|
||||
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
|
||||
"idpOidcConfigure": "Configuración OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Menú de navegación",
|
||||
"navbarDescription": "Menú de navegación principal para la aplicación",
|
||||
"navbarDocsLink": "Documentación",
|
||||
"commercialEdition": "Edición Comercial",
|
||||
"otpErrorEnable": "No se puede habilitar 2FA",
|
||||
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
|
||||
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Todos los usuarios",
|
||||
"sidebarIdentityProviders": "Proveedores de identidad",
|
||||
"sidebarLicense": "Licencia",
|
||||
"sidebarClients": "Clientes (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Dominios",
|
||||
"enableDockerSocket": "Habilitar Plano Docker",
|
||||
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Nivel Gratis",
|
||||
"billingWarningOverLimit": "Advertencia: Has excedido uno o más límites de uso. Tus sitios no se conectarán hasta que modifiques tu suscripción o ajustes tu uso.",
|
||||
"billingUsageLimitsOverview": "Descripción general de los límites de uso",
|
||||
"billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@pangolin.net.",
|
||||
"billingDataUsage": "Uso de datos",
|
||||
"billingOnlineTime": "Tiempo en línea del sitio",
|
||||
"billingUsers": "Usuarios activos",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.",
|
||||
"twoFactor": "Autenticación de dos factores",
|
||||
"adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.",
|
||||
"continueToApplication": "Continuar a la aplicación",
|
||||
"securityKeyAdd": "Agregar llave de seguridad",
|
||||
"securityKeyRegisterTitle": "Registrar nueva llave de seguridad",
|
||||
"securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Proxy externo habilitado",
|
||||
"addNewTarget": "Agregar nuevo destino",
|
||||
"targetsList": "Lista de destinos",
|
||||
"advancedMode": "Modo avanzado",
|
||||
"targetErrorDuplicateTargetFound": "Se encontró un destino duplicado",
|
||||
"healthCheckHealthy": "Saludable",
|
||||
"healthCheckUnhealthy": "No saludable",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Error de inicio de sesión automático",
|
||||
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
||||
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Administrar Nodos Autogestionados",
|
||||
"remoteExitNodeDescription": "Administrar nodos para extender la conectividad de red",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Nodos remotos",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nodos",
|
||||
"searchRemoteExitNodes": "Buscar nodos...",
|
||||
"remoteExitNodeAdd": "Añadir Nodo",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.",
|
||||
"remoteExitNodeConfirmDelete": "Confirmar eliminar nodo",
|
||||
"remoteExitNodeDelete": "Eliminar Nodo",
|
||||
"sidebarRemoteExitNodes": "Nodos",
|
||||
"sidebarRemoteExitNodes": "Nodos remotos",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Crear Nodo",
|
||||
"description": "Crear un nuevo nodo para extender la conectividad de red",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Página auth actualizada correctamente",
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -6,45 +6,45 @@
|
||||
"setupOrgName": "Nom de l'organisation",
|
||||
"orgDisplayName": "Ceci est le nom d'affichage de votre organisation.",
|
||||
"orgId": "ID de l'organisation",
|
||||
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.",
|
||||
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
||||
"setupIdentifierMessage": "Ceci est l'identifiant de votre organisation. Il est différent du nom affiché.",
|
||||
"setupErrorIdentifier": "Cet identifiant est déjà pris. Veuillez en choisir un autre.",
|
||||
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
||||
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
||||
"welcome": "Bienvenue à Pangolin",
|
||||
"welcome": "Bienvenue sur Pangolin",
|
||||
"welcomeTo": "Bienvenue chez",
|
||||
"componentsCreateOrg": "Créer une organisation",
|
||||
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
||||
"componentsMember": "Vous {count, plural, =0 {n'} other {}}êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
|
||||
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"dismiss": "Refuser",
|
||||
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
|
||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.",
|
||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
|
||||
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
||||
"goHome": "Retour à la maison",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
|
||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||
"inviteLoginUser": "Assurez-vous d'etre bien connecté au bon compte.",
|
||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
|
||||
"inviteCreateUser": "Vous n'avez aucun compte, veuillez en créer un.",
|
||||
"goHome": "Retour à l'accueil",
|
||||
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
|
||||
"createAnAccount": "Créer un compte",
|
||||
"inviteNotAccepted": "Invitation non acceptée",
|
||||
"authCreateAccount": "Créez un compte pour commencer",
|
||||
"authNoAccount": "Vous n'avez pas de compte ?",
|
||||
"email": "Courriel",
|
||||
"email": "Adresse email",
|
||||
"password": "Mot de passe",
|
||||
"confirmPassword": "Confirmer le mot de passe",
|
||||
"createAccount": "Créer un compte",
|
||||
"viewSettings": "Afficher les paramètres",
|
||||
"delete": "Supprimez",
|
||||
"delete": "Supprimer",
|
||||
"name": "Nom",
|
||||
"online": "En ligne",
|
||||
"offline": "Hors ligne",
|
||||
"site": "Site",
|
||||
"dataIn": "Données dans",
|
||||
"dataOut": "Données épuisées",
|
||||
"dataIn": "Données reçues",
|
||||
"dataOut": "Données émises",
|
||||
"connectionType": "Type de connexion",
|
||||
"tunnelType": "Type de tunnel",
|
||||
"local": "Locale",
|
||||
"edit": "Editer",
|
||||
"edit": "Modifier",
|
||||
"siteConfirmDelete": "Confirmer la suppression du site",
|
||||
"siteDelete": "Supprimer le site",
|
||||
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.",
|
||||
@@ -64,11 +64,11 @@
|
||||
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
|
||||
"siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.",
|
||||
"siteLoadWGConfig": "Chargement de la configuration WireGuard...",
|
||||
"siteDocker": "Développer les détails du déploiement Docker",
|
||||
"siteDocker": "Afficher les détails du déploiement Docker",
|
||||
"toggle": "Activer/désactiver",
|
||||
"dockerCompose": "Composition Docker",
|
||||
"dockerRun": "Exécution Docker",
|
||||
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"dockerRun": "Docker Run",
|
||||
"siteLearnLocal": "Les sites locaux ne permettent pas d'utiliser les tunnel, en savoir plus",
|
||||
"siteConfirmCopy": "J'ai copié la configuration",
|
||||
"searchSitesProgress": "Rechercher des sites...",
|
||||
"siteAdd": "Ajouter un site",
|
||||
@@ -79,9 +79,9 @@
|
||||
"operatingSystem": "Système d'exploitation",
|
||||
"commands": "Commandes",
|
||||
"recommended": "Recommandé",
|
||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
||||
"siteRunsInDocker": "Exécute dans Docker",
|
||||
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows",
|
||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Newt se base sur WireGuard et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
||||
"siteRunsInDocker": "S'exécute dans Docker",
|
||||
"siteRunsInShell": "S'exécute en shell sur macOS, Linux et Windows",
|
||||
"siteErrorDelete": "Erreur lors de la suppression du site",
|
||||
"siteErrorUpdate": "Impossible de mettre à jour le site",
|
||||
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
|
||||
@@ -89,18 +89,18 @@
|
||||
"siteUpdatedDescription": "Le site a été mis à jour.",
|
||||
"siteGeneralDescription": "Configurer les paramètres généraux de ce site",
|
||||
"siteSettingDescription": "Configurer les paramètres de votre site",
|
||||
"siteSetting": "Réglages {siteName}",
|
||||
"siteSetting": "Réglages de {siteName}",
|
||||
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
|
||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
|
||||
"siteWg": "WireGuard basique",
|
||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteLocalDescriptionSaas": "Ressources locales seulement. Pas de tunneling. Seulement disponible sur les noeuds distants",
|
||||
"siteSeeAll": "Voir tous les sites",
|
||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||
"siteNewtCredentials": "Identifiants Newt",
|
||||
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur",
|
||||
"siteNewtCredentialsDescription": "C'est comme cela que Newt s'authentifiera avec le serveur",
|
||||
"siteCredentialsSave": "Enregistrez vos identifiants",
|
||||
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
|
||||
"siteInfo": "Informations sur le site",
|
||||
@@ -112,7 +112,7 @@
|
||||
"shareErrorDelete": "Impossible de supprimer le lien",
|
||||
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
||||
"shareDeleted": "Lien supprimé",
|
||||
"shareDeletedDescription": "Le lien a été supprimé",
|
||||
"shareDeletedDescription": "Le lien de partage a été supprimé",
|
||||
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
||||
"accessToken": "Jeton d'accès",
|
||||
"usageExamples": "Exemples d'utilisation",
|
||||
@@ -134,7 +134,7 @@
|
||||
"shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.",
|
||||
"shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.",
|
||||
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.",
|
||||
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
|
||||
"shareTokenUsage": "Voir l'utilisation du jeton d'accès",
|
||||
"createLink": "Créer un lien",
|
||||
"resourcesNotFound": "Aucune ressource trouvée",
|
||||
"resourceSearch": "Rechercher des ressources",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Créé le",
|
||||
"proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.",
|
||||
"proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.",
|
||||
"proxyEnableSSL": "Activer SSL (https)",
|
||||
"proxyEnableSSL": "Activer SSL",
|
||||
"proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurer les cibles",
|
||||
"targetErrorFetch": "Échec de la récupération des cibles",
|
||||
"targetErrorFetchDescription": "Une erreur s'est produite lors de la récupération des cibles",
|
||||
"siteErrorFetch": "Échec de la récupération de la ressource",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Configuration sécurisée de connexion",
|
||||
"targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource",
|
||||
"targetTlsSettingsAdvanced": "Paramètres TLS avancés",
|
||||
"targetTlsSni": "Nom de serveur TLS (SNI)",
|
||||
"targetTlsSni": "Nom du serveur TLS",
|
||||
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
||||
"targetTlsSubmit": "Enregistrer les paramètres",
|
||||
"targets": "Configuration des cibles",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
||||
"methodSelect": "Sélectionner la méthode",
|
||||
"targetSubmit": "Ajouter une cible",
|
||||
"targetNoOne": "Aucune cible. Ajoutez une cible en utilisant le formulaire.",
|
||||
"targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.",
|
||||
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
||||
"targetsSubmit": "Enregistrer les cibles",
|
||||
"addTarget": "Ajouter une cible",
|
||||
"targetErrorInvalidIp": "Adresse IP invalide",
|
||||
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
|
||||
"targetErrorInvalidPort": "Port invalide",
|
||||
"targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide",
|
||||
"targetErrorNoSite": "Aucun site sélectionné",
|
||||
"targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible",
|
||||
"targetCreated": "Cible créée",
|
||||
"targetCreatedDescription": "La cible a été créée avec succès",
|
||||
"targetErrorCreate": "Impossible de créer la cible",
|
||||
"targetErrorCreateDescription": "Une erreur s'est produite lors de la création de la cible",
|
||||
"save": "Enregistrer",
|
||||
"proxyAdditional": "Paramètres de proxy supplémentaires",
|
||||
"proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy",
|
||||
"proxyCustomHeader": "En-tête Host personnalisé",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Admin Serveur - Pangolin",
|
||||
"licenseTierProfessional": "Licence Professionnelle",
|
||||
"licenseTierEnterprise": "Licence Entreprise",
|
||||
"licenseTierCommercial": "Licence commerciale",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Sous licence",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
|
||||
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs",
|
||||
"idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.",
|
||||
"licenseBadge": "Professionnel",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Type de fournisseur",
|
||||
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
|
||||
"idpOidcConfigure": "Configuration OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Menu de navigation",
|
||||
"navbarDescription": "Menu de navigation principal de l'application",
|
||||
"navbarDocsLink": "Documentation",
|
||||
"commercialEdition": "Édition Commerciale",
|
||||
"otpErrorEnable": "Impossible d'activer l'A2F",
|
||||
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
|
||||
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Tous les utilisateurs",
|
||||
"sidebarIdentityProviders": "Fournisseurs d'identité",
|
||||
"sidebarLicense": "Licence",
|
||||
"sidebarClients": "Clients (Bêta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domaines",
|
||||
"enableDockerSocket": "Activer le Plan Docker",
|
||||
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
||||
@@ -1220,7 +1234,7 @@
|
||||
"billing": "Facturation",
|
||||
"orgBillingDescription": "Gérez vos informations de facturation et vos abonnements",
|
||||
"github": "GitHub",
|
||||
"pangolinHosted": "Pangolin Hébergement",
|
||||
"pangolinHosted": "Hebergé par Pangolin",
|
||||
"fossorial": "Fossorial",
|
||||
"completeAccountSetup": "Complétez la configuration du compte",
|
||||
"completeAccountSetupDescription": "Définissez votre mot de passe pour commencer",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Niveau gratuit",
|
||||
"billingWarningOverLimit": "Attention : Vous avez dépassé une ou plusieurs limites d'utilisation. Vos sites ne se connecteront pas tant que vous n'avez pas modifié votre abonnement ou ajusté votre utilisation.",
|
||||
"billingUsageLimitsOverview": "Vue d'ensemble des limites d'utilisation",
|
||||
"billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@fossorial.io.",
|
||||
"billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@pangolin.net.",
|
||||
"billingDataUsage": "Utilisation des données",
|
||||
"billingOnlineTime": "Temps en ligne du site",
|
||||
"billingUsers": "Utilisateurs actifs",
|
||||
@@ -1302,7 +1316,7 @@
|
||||
"billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque nœud géré dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.",
|
||||
"domainNotFound": "Domaine introuvable",
|
||||
"domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.",
|
||||
"failed": "Échec",
|
||||
"failed": "Erreur",
|
||||
"createNewOrgDescription": "Créer une nouvelle organisation",
|
||||
"organization": "Organisation",
|
||||
"port": "Port",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.",
|
||||
"twoFactor": "Authentification à deux facteurs",
|
||||
"adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.",
|
||||
"continueToApplication": "Continuer vers l'application",
|
||||
"securityKeyAdd": "Ajouter une clé de sécurité",
|
||||
"securityKeyRegisterTitle": "Enregistrer une nouvelle clé de sécurité",
|
||||
"securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier",
|
||||
@@ -1357,7 +1370,7 @@
|
||||
"createDomainARecords": "Enregistrements A",
|
||||
"createDomainRecordNumber": "Enregistrement {number}",
|
||||
"createDomainTxtRecords": "Enregistrements TXT",
|
||||
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
|
||||
"createDomainSaveTheseRecords": "Sauvegardez ces enregistrements",
|
||||
"createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.",
|
||||
"createDomainDnsPropagation": "Propagation DNS",
|
||||
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Proxy externe activé",
|
||||
"addNewTarget": "Ajouter une nouvelle cible",
|
||||
"targetsList": "Liste des cibles",
|
||||
"advancedMode": "Mode Avancé",
|
||||
"targetErrorDuplicateTargetFound": "Cible en double trouvée",
|
||||
"healthCheckHealthy": "Sain",
|
||||
"healthCheckUnhealthy": "En mauvaise santé",
|
||||
@@ -1431,7 +1445,7 @@
|
||||
"IntervalSeconds": "Intervalle sain",
|
||||
"timeoutSeconds": "Délai",
|
||||
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
||||
"retryAttempts": "Tentatives de réessai",
|
||||
"retryAttempts": "Tentatives",
|
||||
"expectedResponseCodes": "Codes de réponse attendus",
|
||||
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
|
||||
"customHeaders": "En-têtes personnalisés",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Erreur de connexion automatique",
|
||||
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
||||
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gérer auto-hébergé",
|
||||
"remoteExitNodeDescription": "Gérer les nœuds pour étendre votre connectivité réseau",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Nœuds distants",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nœuds",
|
||||
"searchRemoteExitNodes": "Rechercher des nœuds...",
|
||||
"remoteExitNodeAdd": "Ajouter un noeud",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.",
|
||||
"remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud",
|
||||
"remoteExitNodeDelete": "Supprimer le noeud",
|
||||
"sidebarRemoteExitNodes": "Nœuds",
|
||||
"sidebarRemoteExitNodes": "Nœuds distants",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Créer un noeud",
|
||||
"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",
|
||||
"healthCheckNotAvailable": "Locale",
|
||||
"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": "Gérer les clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valide jusqu'au",
|
||||
"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": "Le prénom est requis",
|
||||
"lastNameRequired": "Le nom est requis",
|
||||
"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": "Utilisation personelle",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"jobTitle": "profession",
|
||||
"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": "Entreprise",
|
||||
"countryOfResidence": "Pays de résidence",
|
||||
"stateProvinceRegion": "État / Province / Région",
|
||||
"postalZipCode": "Code postal",
|
||||
"companyWebsite": "Site de l'entreprise",
|
||||
"companyPhoneNumber": "Numéro de téléphone professionnel",
|
||||
"country": "Pays",
|
||||
"phoneNumberOptional": "Numéro de téléphone (optionnel)",
|
||||
"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": "Fermer",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"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": "Nom de l'instance",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Préfix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Sauvegarder",
|
||||
"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": "Préfix",
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Vedi Tutti I Siti",
|
||||
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
||||
"siteNewtCredentials": "Credenziali Newt",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Creato Il",
|
||||
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
|
||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||
"proxyEnableSSL": "Abilita SSL (https)",
|
||||
"proxyEnableSSL": "Abilita SSL",
|
||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configura Obiettivi",
|
||||
"targetErrorFetch": "Impossibile recuperare i target",
|
||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Configurazione Connessione Sicura",
|
||||
"targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa",
|
||||
"targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate",
|
||||
"targetTlsSni": "Nome Server TLS (SNI)",
|
||||
"targetTlsSni": "Nome Server Tls",
|
||||
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
||||
"targetTlsSubmit": "Salva Impostazioni",
|
||||
"targets": "Configurazione Target",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
||||
"methodSelect": "Seleziona metodo",
|
||||
"targetSubmit": "Aggiungi Target",
|
||||
"targetNoOne": "Nessun target. Aggiungi un target usando il modulo.",
|
||||
"targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.",
|
||||
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
||||
"targetsSubmit": "Salva Target",
|
||||
"addTarget": "Aggiungi Target",
|
||||
"targetErrorInvalidIp": "Indirizzo IP non valido",
|
||||
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
|
||||
"targetErrorInvalidPort": "Porta non valida",
|
||||
"targetErrorInvalidPortDescription": "Inserisci un numero di porta valido",
|
||||
"targetErrorNoSite": "Nessun sito selezionato",
|
||||
"targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo",
|
||||
"targetCreated": "Destinazione creata",
|
||||
"targetCreatedDescription": "L'obiettivo è stato creato con successo",
|
||||
"targetErrorCreate": "Impossibile creare l'obiettivo",
|
||||
"targetErrorCreateDescription": "Si è verificato un errore durante la creazione del target",
|
||||
"save": "Salva",
|
||||
"proxyAdditional": "Impostazioni Proxy Aggiuntive",
|
||||
"proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy",
|
||||
"proxyCustomHeader": "Intestazione Host Personalizzata",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolina",
|
||||
"licenseTierProfessional": "Licenza Professional",
|
||||
"licenseTierEnterprise": "Licenza Enterprise",
|
||||
"licenseTierCommercial": "Licenza Commerciale",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Con Licenza",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Un nome visualizzato per questo provider di identità",
|
||||
"idpAutoProvisionUsers": "Provisioning Automatico Utenti",
|
||||
"idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.",
|
||||
"licenseBadge": "Professionista",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo di Provider",
|
||||
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
|
||||
"idpOidcConfigure": "Configurazione OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Menu di Navigazione",
|
||||
"navbarDescription": "Menu di navigazione principale dell'applicazione",
|
||||
"navbarDocsLink": "Documentazione",
|
||||
"commercialEdition": "Edizione Commerciale",
|
||||
"otpErrorEnable": "Impossibile abilitare 2FA",
|
||||
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
|
||||
"otpSetupCheckCode": "Inserisci un codice a 6 cifre",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Tutti Gli Utenti",
|
||||
"sidebarIdentityProviders": "Fornitori Di Identità",
|
||||
"sidebarLicense": "Licenza",
|
||||
"sidebarClients": "Clienti (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domini",
|
||||
"enableDockerSocket": "Abilita Progetto Docker",
|
||||
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Piano Gratuito",
|
||||
"billingWarningOverLimit": "Avviso: Hai superato uno o più limiti di utilizzo. I tuoi siti non si connetteranno finché non modifichi il tuo abbonamento o non adegui il tuo utilizzo.",
|
||||
"billingUsageLimitsOverview": "Panoramica dei Limiti di Utilizzo",
|
||||
"billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@pangolin.net.",
|
||||
"billingDataUsage": "Utilizzo dei Dati",
|
||||
"billingOnlineTime": "Tempo Online del Sito",
|
||||
"billingUsers": "Utenti Attivi",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.",
|
||||
"twoFactor": "Autenticazione a Due Fattori",
|
||||
"adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.",
|
||||
"continueToApplication": "Continua con l'applicazione",
|
||||
"securityKeyAdd": "Aggiungi Chiave di Sicurezza",
|
||||
"securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza",
|
||||
"securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Proxy Esterno Abilitato",
|
||||
"addNewTarget": "Aggiungi Nuovo Target",
|
||||
"targetsList": "Elenco dei Target",
|
||||
"advancedMode": "Modalità Avanzata",
|
||||
"targetErrorDuplicateTargetFound": "Target duplicato trovato",
|
||||
"healthCheckHealthy": "Sano",
|
||||
"healthCheckUnhealthy": "Non Sano",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Errore di Accesso Automatico",
|
||||
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
||||
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gestisci Self-Hosted",
|
||||
"remoteExitNodeDescription": "Gestisci i nodi per estendere la connettività di rete",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Nodi Remoti",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nodi",
|
||||
"searchRemoteExitNodes": "Cerca nodi...",
|
||||
"remoteExitNodeAdd": "Aggiungi Nodo",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.",
|
||||
"remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo",
|
||||
"remoteExitNodeDelete": "Elimina Nodo",
|
||||
"sidebarRemoteExitNodes": "Nodi",
|
||||
"sidebarRemoteExitNodes": "Nodi Remoti",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Crea Nodo",
|
||||
"description": "Crea un nuovo nodo per estendere la connettività di rete",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Pagina di autenticazione aggiornata con successo",
|
||||
"healthCheckNotAvailable": "Locale",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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 설정이 필요합니다.",
|
||||
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "모든 사이트 보기",
|
||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||
"siteNewtCredentials": "Newt 자격 증명",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "생성일",
|
||||
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
|
||||
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
|
||||
"proxyEnableSSL": "SSL 활성화 (https)",
|
||||
"proxyEnableSSL": "SSL 활성화",
|
||||
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.",
|
||||
"target": "대상",
|
||||
"configureTarget": "대상 구성",
|
||||
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
|
||||
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
|
||||
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "보안 연결 구성",
|
||||
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
|
||||
"targetTlsSettingsAdvanced": "고급 TLS 설정",
|
||||
"targetTlsSni": "TLS 서버 이름 (SNI)",
|
||||
"targetTlsSni": "TLS 서버 이름",
|
||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||
"targetTlsSubmit": "설정 저장",
|
||||
"targets": "대상 구성",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||
"methodSelect": "선택 방법",
|
||||
"targetSubmit": "대상 추가",
|
||||
"targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.",
|
||||
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.",
|
||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||
"targetsSubmit": "대상 저장",
|
||||
"addTarget": "대상 추가",
|
||||
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
|
||||
"targetErrorNoSite": "선택된 사이트 없음",
|
||||
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
|
||||
"targetCreated": "대상 생성",
|
||||
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
|
||||
"targetErrorCreate": "대상 생성 실패",
|
||||
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
|
||||
"save": "저장",
|
||||
"proxyAdditional": "추가 프록시 설정",
|
||||
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
|
||||
"proxyCustomHeader": "사용자 정의 호스트 헤더",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
||||
"licenseTierProfessional": "전문 라이센스",
|
||||
"licenseTierEnterprise": "기업 라이선스",
|
||||
"licenseTierCommercial": "상업용 라이선스",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "라이센스",
|
||||
"yes": "예",
|
||||
"no": "아니요",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
||||
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
||||
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
||||
"licenseBadge": "전문가",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "제공자 유형",
|
||||
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 구성",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "탐색 메뉴",
|
||||
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
|
||||
"navbarDocsLink": "문서",
|
||||
"commercialEdition": "상업용 에디션",
|
||||
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
|
||||
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
|
||||
"otpSetupCheckCode": "6자리 코드를 입력하세요",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "모든 사용자",
|
||||
"sidebarIdentityProviders": "신원 공급자",
|
||||
"sidebarLicense": "라이선스",
|
||||
"sidebarClients": "클라이언트 (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "도메인",
|
||||
"enableDockerSocket": "Docker 청사진 활성화",
|
||||
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "무료 티어",
|
||||
"billingWarningOverLimit": "경고: 하나 이상의 사용 한도를 초과했습니다. 구독을 수정하거나 사용량을 조정하기 전까지 사이트는 연결되지 않습니다.",
|
||||
"billingUsageLimitsOverview": "사용 한도 개요",
|
||||
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@fossorial.io로 연락하십시오.",
|
||||
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.",
|
||||
"billingDataUsage": "데이터 사용량",
|
||||
"billingOnlineTime": "사이트 온라인 시간",
|
||||
"billingUsers": "활성 사용자",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
|
||||
"twoFactor": "이중 인증",
|
||||
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
|
||||
"continueToApplication": "응용 프로그램으로 계속",
|
||||
"securityKeyAdd": "보안 키 추가",
|
||||
"securityKeyRegisterTitle": "새 보안 키 등록",
|
||||
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "외부 프록시 활성화됨",
|
||||
"addNewTarget": "새 대상 추가",
|
||||
"targetsList": "대상 목록",
|
||||
"advancedMode": "고급 모드",
|
||||
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
||||
"healthCheckHealthy": "정상",
|
||||
"healthCheckUnhealthy": "비정상",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "자동 로그인 오류",
|
||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "관리 자체 호스팅",
|
||||
"remoteExitNodeDescription": "네트워크 연결성을 확장하기 위해 노드를 관리하세요",
|
||||
"remoteExitNodeManageRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "노드",
|
||||
"searchRemoteExitNodes": "노드 검색...",
|
||||
"remoteExitNodeAdd": "노드 추가",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.",
|
||||
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
|
||||
"remoteExitNodeDelete": "노드 삭제",
|
||||
"sidebarRemoteExitNodes": "노드",
|
||||
"sidebarRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "노드 생성",
|
||||
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
|
||||
"healthCheckNotAvailable": "로컬",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
||||
"siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.",
|
||||
"siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Se alle områder",
|
||||
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
||||
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Opprettet",
|
||||
"proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.",
|
||||
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
|
||||
"proxyEnableSSL": "Aktiver SSL (https)",
|
||||
"proxyEnableSSL": "Aktiver SSL",
|
||||
"proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Konfigurer mål",
|
||||
"targetErrorFetch": "Kunne ikke hente mål",
|
||||
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
|
||||
"siteErrorFetch": "Klarte ikke å hente ressurs",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Sikker tilkoblings-konfigurasjon",
|
||||
"targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din",
|
||||
"targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger",
|
||||
"targetTlsSni": "TLS Servernavn (SNI)",
|
||||
"targetTlsSni": "TLS servernavn",
|
||||
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
||||
"targetTlsSubmit": "Lagre innstillinger",
|
||||
"targets": "Målkonfigurasjon",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
||||
"methodSelect": "Velg metode",
|
||||
"targetSubmit": "Legg til mål",
|
||||
"targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.",
|
||||
"targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.",
|
||||
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
||||
"targetsSubmit": "Lagre mål",
|
||||
"addTarget": "Legg til mål",
|
||||
"targetErrorInvalidIp": "Ugyldig IP-adresse",
|
||||
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
|
||||
"targetErrorInvalidPort": "Ugyldig port",
|
||||
"targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer",
|
||||
"targetErrorNoSite": "Ingen nettsted valgt",
|
||||
"targetErrorNoSiteDescription": "Velg et nettsted for målet",
|
||||
"targetCreated": "Mål opprettet",
|
||||
"targetCreatedDescription": "Målet har blitt opprettet",
|
||||
"targetErrorCreate": "Kunne ikke opprette målet",
|
||||
"targetErrorCreateDescription": "Det oppstod en feil under oppretting av målet",
|
||||
"save": "Lagre",
|
||||
"proxyAdditional": "Ytterligere Proxy-innstillinger",
|
||||
"proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger",
|
||||
"proxyCustomHeader": "Tilpasset verts-header",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
"licenseTierProfessional": "Profesjonell lisens",
|
||||
"licenseTierEnterprise": "Bedriftslisens",
|
||||
"licenseTierCommercial": "Kommersiell lisens",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Lisensiert",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
|
||||
"idpAutoProvisionUsers": "Automatisk brukerklargjøring",
|
||||
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
|
||||
"licenseBadge": "Profesjonell",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Leverandørtype",
|
||||
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
|
||||
"idpOidcConfigure": "OAuth2/OIDC-konfigurasjon",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Navigasjonsmeny",
|
||||
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
|
||||
"navbarDocsLink": "Dokumentasjon",
|
||||
"commercialEdition": "Kommersiell utgave",
|
||||
"otpErrorEnable": "Kunne ikke aktivere 2FA",
|
||||
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
|
||||
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Alle brukere",
|
||||
"sidebarIdentityProviders": "Identitetsleverandører",
|
||||
"sidebarLicense": "Lisens",
|
||||
"sidebarClients": "Klienter (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domener",
|
||||
"enableDockerSocket": "Aktiver Docker blåkopi",
|
||||
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Gratis nivå",
|
||||
"billingWarningOverLimit": "Advarsel: Du har overskredet en eller flere bruksgrenser. Nettstedene dine vil ikke koble til før du endrer abonnementet ditt eller justerer bruken.",
|
||||
"billingUsageLimitsOverview": "Oversikt over bruksgrenser",
|
||||
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@fossorial.io.",
|
||||
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@pangolin.net.",
|
||||
"billingDataUsage": "Databruk",
|
||||
"billingOnlineTime": "Online tid for nettsteder",
|
||||
"billingUsers": "Aktive brukere",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.",
|
||||
"twoFactor": "Tofaktorautentisering",
|
||||
"adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.",
|
||||
"continueToApplication": "Fortsett til applikasjonen",
|
||||
"securityKeyAdd": "Legg til sikkerhetsnøkkel",
|
||||
"securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel",
|
||||
"securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Ekstern proxy aktivert",
|
||||
"addNewTarget": "Legg til nytt mål",
|
||||
"targetsList": "Liste over mål",
|
||||
"advancedMode": "Avansert modus",
|
||||
"targetErrorDuplicateTargetFound": "Duplikat av mål funnet",
|
||||
"healthCheckHealthy": "Sunn",
|
||||
"healthCheckUnhealthy": "Usunn",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Feil ved automatisk innlogging",
|
||||
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
|
||||
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Administrer Selv-Hostet",
|
||||
"remoteExitNodeDescription": "Administrer noder for å forlenge nettverkstilkoblingen din",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Eksterne Noder",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Noder",
|
||||
"searchRemoteExitNodes": "Søk noder...",
|
||||
"remoteExitNodeAdd": "Legg til Node",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.",
|
||||
"remoteExitNodeConfirmDelete": "Bekreft sletting av Node",
|
||||
"remoteExitNodeDelete": "Slett Node",
|
||||
"sidebarRemoteExitNodes": "Noder",
|
||||
"sidebarRemoteExitNodes": "Eksterne Noder",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Opprett node",
|
||||
"description": "Opprett en ny node for å utvide nettverkstilkoblingen din",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Godkjenningsside oppdatert",
|
||||
"healthCheckNotAvailable": "Lokal",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
|
||||
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Alle sites bekijken",
|
||||
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
||||
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Aangemaakt op",
|
||||
"proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.",
|
||||
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
|
||||
"proxyEnableSSL": "SSL (https) inschakelen",
|
||||
"proxyEnableSSL": "SSL inschakelen",
|
||||
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Doelstellingen configureren",
|
||||
"targetErrorFetch": "Ophalen van doelen mislukt",
|
||||
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
|
||||
"siteErrorFetch": "Mislukt om resource op te halen",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "HTTPS & TLS instellingen",
|
||||
"targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren",
|
||||
"targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen",
|
||||
"targetTlsSni": "TLS Server Naam (SNI)",
|
||||
"targetTlsSni": "TLS servernaam",
|
||||
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
||||
"targetTlsSubmit": "Instellingen opslaan",
|
||||
"targets": "Doelstellingen configuratie",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||
"methodSelect": "Selecteer methode",
|
||||
"targetSubmit": "Doelwit toevoegen",
|
||||
"targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.",
|
||||
"targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.",
|
||||
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
||||
"targetsSubmit": "Doelstellingen opslaan",
|
||||
"addTarget": "Doelwit toevoegen",
|
||||
"targetErrorInvalidIp": "Ongeldig IP-adres",
|
||||
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
|
||||
"targetErrorInvalidPort": "Ongeldige poort",
|
||||
"targetErrorInvalidPortDescription": "Voer een geldig poortnummer in",
|
||||
"targetErrorNoSite": "Geen site geselecteerd",
|
||||
"targetErrorNoSiteDescription": "Selecteer een site voor het doel",
|
||||
"targetCreated": "Doel aangemaakt",
|
||||
"targetCreatedDescription": "Doel is succesvol aangemaakt",
|
||||
"targetErrorCreate": "Kan doel niet aanmaken",
|
||||
"targetErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van het doel",
|
||||
"save": "Opslaan",
|
||||
"proxyAdditional": "Extra Proxy-instellingen",
|
||||
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
|
||||
"proxyCustomHeader": "Aangepaste Host-header",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Serverbeheer - Pangolin",
|
||||
"licenseTierProfessional": "Professionele licentie",
|
||||
"licenseTierEnterprise": "Enterprise Licentie",
|
||||
"licenseTierCommercial": "Commerciële licentie",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Gelicentieerd",
|
||||
"yes": "ja",
|
||||
"no": "Neen",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
|
||||
"idpAutoProvisionUsers": "Auto Provisie Gebruikers",
|
||||
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
|
||||
"licenseBadge": "Professioneel",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Type provider",
|
||||
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
|
||||
"idpOidcConfigure": "OAuth2/OIDC configuratie",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Navigatiemenu",
|
||||
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
|
||||
"navbarDocsLink": "Documentatie",
|
||||
"commercialEdition": "Commerciële editie",
|
||||
"otpErrorEnable": "Kan 2FA niet inschakelen",
|
||||
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
|
||||
"otpSetupCheckCode": "Voer een 6-cijferige code in",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Alle gebruikers",
|
||||
"sidebarIdentityProviders": "Identiteit aanbieders",
|
||||
"sidebarLicense": "Licentie",
|
||||
"sidebarClients": "Clients (Bèta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domeinen",
|
||||
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
||||
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Gratis Niveau",
|
||||
"billingWarningOverLimit": "Waarschuwing: U hebt een of meer gebruikslimieten overschreden. Uw sites maken geen verbinding totdat u uw abonnement aanpast of uw gebruik aanpast.",
|
||||
"billingUsageLimitsOverview": "Overzicht gebruikslimieten",
|
||||
"billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@fossorial.io.",
|
||||
"billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@pangolin.net.",
|
||||
"billingDataUsage": "Gegevensgebruik",
|
||||
"billingOnlineTime": "Site Online Tijd",
|
||||
"billingUsers": "Actieve Gebruikers",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.",
|
||||
"twoFactor": "Tweestapsverificatie",
|
||||
"adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.",
|
||||
"continueToApplication": "Doorgaan naar applicatie",
|
||||
"securityKeyAdd": "Beveiligingssleutel toevoegen",
|
||||
"securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren",
|
||||
"securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Externe Proxy Ingeschakeld",
|
||||
"addNewTarget": "Voeg nieuw doelwit toe",
|
||||
"targetsList": "Lijst met doelen",
|
||||
"advancedMode": "Geavanceerde modus",
|
||||
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
|
||||
"healthCheckHealthy": "Gezond",
|
||||
"healthCheckUnhealthy": "Ongezond",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Auto Login Fout",
|
||||
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Beheer Zelf-Gehoste",
|
||||
"remoteExitNodeDescription": "Beheer knooppunten om uw netwerkverbinding uit te breiden",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Externe knooppunten",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nodes",
|
||||
"searchRemoteExitNodes": "Knooppunten zoeken...",
|
||||
"remoteExitNodeAdd": "Voeg node toe",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.",
|
||||
"remoteExitNodeConfirmDelete": "Bevestig verwijderen node",
|
||||
"remoteExitNodeDelete": "Knoop verwijderen",
|
||||
"sidebarRemoteExitNodes": "Nodes",
|
||||
"sidebarRemoteExitNodes": "Externe knooppunten",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Maak node",
|
||||
"description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Auth-pagina succesvol bijgewerkt",
|
||||
"healthCheckNotAvailable": "Lokaal",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
|
||||
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Zobacz wszystkie witryny",
|
||||
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
||||
"siteNewtCredentials": "Aktualne dane logowania",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Utworzono",
|
||||
"proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.",
|
||||
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
|
||||
"proxyEnableSSL": "Włącz SSL (https)",
|
||||
"proxyEnableSSL": "Włącz SSL",
|
||||
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Konfiguruj Targety",
|
||||
"targetErrorFetch": "Nie udało się pobrać celów",
|
||||
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
|
||||
"siteErrorFetch": "Nie udało się pobrać zasobu",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Konfiguracja bezpiecznego połączenia",
|
||||
"targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu",
|
||||
"targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS",
|
||||
"targetTlsSni": "Nazwa serwera TLS (SNI)",
|
||||
"targetTlsSni": "Nazwa serwera TLS",
|
||||
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
||||
"targetTlsSubmit": "Zapisz ustawienia",
|
||||
"targets": "Konfiguracja celów",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
||||
"methodSelect": "Wybierz metodę",
|
||||
"targetSubmit": "Dodaj cel",
|
||||
"targetNoOne": "Brak celów. Dodaj cel używając formularza.",
|
||||
"targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.",
|
||||
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
||||
"targetsSubmit": "Zapisz cele",
|
||||
"addTarget": "Dodaj cel",
|
||||
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
|
||||
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
|
||||
"targetErrorInvalidPort": "Nieprawidłowy port",
|
||||
"targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu",
|
||||
"targetErrorNoSite": "Nie wybrano witryny",
|
||||
"targetErrorNoSiteDescription": "Wybierz witrynę docelową",
|
||||
"targetCreated": "Cel utworzony",
|
||||
"targetCreatedDescription": "Cel został utworzony pomyślnie",
|
||||
"targetErrorCreate": "Nie udało się utworzyć celu",
|
||||
"targetErrorCreateDescription": "Wystąpił błąd podczas tworzenia celu",
|
||||
"save": "Zapisz",
|
||||
"proxyAdditional": "Dodatkowe ustawienia proxy",
|
||||
"proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy",
|
||||
"proxyCustomHeader": "Niestandardowy nagłówek hosta",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Administrator serwera - Pangolin",
|
||||
"licenseTierProfessional": "Licencja Professional",
|
||||
"licenseTierEnterprise": "Licencja Enterprise",
|
||||
"licenseTierCommercial": "Licencja handlowa",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Licencjonowany",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
|
||||
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników",
|
||||
"idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.",
|
||||
"licenseBadge": "Profesjonalny",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Typ dostawcy",
|
||||
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
|
||||
"idpOidcConfigure": "Konfiguracja OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Menu nawigacyjne",
|
||||
"navbarDescription": "Główne menu nawigacyjne aplikacji",
|
||||
"navbarDocsLink": "Dokumentacja",
|
||||
"commercialEdition": "Edycja komercyjna",
|
||||
"otpErrorEnable": "Nie można włączyć 2FA",
|
||||
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
|
||||
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Wszyscy użytkownicy",
|
||||
"sidebarIdentityProviders": "Dostawcy tożsamości",
|
||||
"sidebarLicense": "Licencja",
|
||||
"sidebarClients": "Klienci (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domeny",
|
||||
"enableDockerSocket": "Włącz schemat dokera",
|
||||
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Darmowy pakiet",
|
||||
"billingWarningOverLimit": "Ostrzeżenie: Przekroczyłeś jeden lub więcej limitów użytkowania. Twoje witryny nie połączą się, dopóki nie zmienisz subskrypcji lub nie dostosujesz użytkowania.",
|
||||
"billingUsageLimitsOverview": "Przegląd Limitów Użytkowania",
|
||||
"billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@pangolin.net.",
|
||||
"billingDataUsage": "Użycie danych",
|
||||
"billingOnlineTime": "Czas Online Strony",
|
||||
"billingUsers": "Aktywni użytkownicy",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.",
|
||||
"twoFactor": "Uwierzytelnianie dwuskładnikowe",
|
||||
"adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.",
|
||||
"continueToApplication": "Kontynuuj do aplikacji",
|
||||
"securityKeyAdd": "Dodaj klucz bezpieczeństwa",
|
||||
"securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa",
|
||||
"securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Zewnętrzny Proxy Włączony",
|
||||
"addNewTarget": "Dodaj nowy cel",
|
||||
"targetsList": "Lista celów",
|
||||
"advancedMode": "Tryb zaawansowany",
|
||||
"targetErrorDuplicateTargetFound": "Znaleziono duplikat celu",
|
||||
"healthCheckHealthy": "Zdrowy",
|
||||
"healthCheckUnhealthy": "Niezdrowy",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Błąd automatycznego logowania",
|
||||
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
||||
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Zarządzaj Samodzielnie-Hostingowane",
|
||||
"remoteExitNodeDescription": "Zarządzaj węzłami w celu rozszerzenia połączenia z siecią",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Zdalne węzły",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Węzły",
|
||||
"searchRemoteExitNodes": "Szukaj węzłów...",
|
||||
"remoteExitNodeAdd": "Dodaj węzeł",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.",
|
||||
"remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła",
|
||||
"remoteExitNodeDelete": "Usuń węzeł",
|
||||
"sidebarRemoteExitNodes": "Węzły",
|
||||
"sidebarRemoteExitNodes": "Zdalne węzły",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Utwórz węzeł",
|
||||
"description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana",
|
||||
"healthCheckNotAvailable": "Lokalny",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||
"siteLocalDescription": "Recursos locais apenas. Sem túneis.",
|
||||
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Ver todos os sites",
|
||||
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
||||
"siteNewtCredentials": "Credenciais Novas",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Criado Em",
|
||||
"proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.",
|
||||
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
|
||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
||||
"proxyEnableSSL": "Habilitar SSL",
|
||||
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurar Alvos",
|
||||
"targetErrorFetch": "Falha ao buscar alvos",
|
||||
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
|
||||
"siteErrorFetch": "Falha ao buscar recurso",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Configuração de conexão segura",
|
||||
"targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso",
|
||||
"targetTlsSettingsAdvanced": "Configurações TLS Avançadas",
|
||||
"targetTlsSni": "Nome do Servidor TLS (SNI)",
|
||||
"targetTlsSni": "Nome do Servidor TLS",
|
||||
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
||||
"targetTlsSubmit": "Guardar Configurações",
|
||||
"targets": "Configuração de Alvos",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
||||
"methodSelect": "Selecionar método",
|
||||
"targetSubmit": "Adicionar Alvo",
|
||||
"targetNoOne": "Sem alvos. Adicione um alvo usando o formulário.",
|
||||
"targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.",
|
||||
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
||||
"targetsSubmit": "Guardar Alvos",
|
||||
"addTarget": "Adicionar Alvo",
|
||||
"targetErrorInvalidIp": "Endereço IP inválido",
|
||||
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
||||
"targetErrorInvalidPort": "Porta inválida",
|
||||
"targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido",
|
||||
"targetErrorNoSite": "Nenhum site selecionado",
|
||||
"targetErrorNoSiteDescription": "Selecione um site para o destino",
|
||||
"targetCreated": "Destino criado",
|
||||
"targetCreatedDescription": "O alvo foi criado com sucesso",
|
||||
"targetErrorCreate": "Falha ao criar destino",
|
||||
"targetErrorCreateDescription": "Ocorreu um erro ao criar o destino",
|
||||
"save": "Guardar",
|
||||
"proxyAdditional": "Configurações Adicionais de Proxy",
|
||||
"proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy",
|
||||
"proxyCustomHeader": "Cabeçalho Host Personalizado",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
|
||||
"licenseTierProfessional": "Licença Profissional",
|
||||
"licenseTierEnterprise": "Licença Empresarial",
|
||||
"licenseTierCommercial": "Licença comercial",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Licenciado",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Um nome de exibição para este provedor de identidade",
|
||||
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores",
|
||||
"idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.",
|
||||
"licenseBadge": "Profissional",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo de Provedor",
|
||||
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
|
||||
"idpOidcConfigure": "Configuração OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Menu de Navegação",
|
||||
"navbarDescription": "Menu de navegação principal da aplicação",
|
||||
"navbarDocsLink": "Documentação",
|
||||
"commercialEdition": "Edição Comercial",
|
||||
"otpErrorEnable": "Não foi possível ativar 2FA",
|
||||
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
|
||||
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Todos os utilizadores",
|
||||
"sidebarIdentityProviders": "Provedores de identidade",
|
||||
"sidebarLicense": "Tipo:",
|
||||
"sidebarClients": "Clientes (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Domínios",
|
||||
"enableDockerSocket": "Habilitar o Diagrama Docker",
|
||||
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Plano Gratuito",
|
||||
"billingWarningOverLimit": "Aviso: Você ultrapassou um ou mais limites de uso. Seus sites não se conectarão até você modificar sua assinatura ou ajustar seu uso.",
|
||||
"billingUsageLimitsOverview": "Visão Geral dos Limites de Uso",
|
||||
"billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@pangolin.net.",
|
||||
"billingDataUsage": "Uso de Dados",
|
||||
"billingOnlineTime": "Tempo Online do Site",
|
||||
"billingUsers": "Usuários Ativos",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.",
|
||||
"twoFactor": "Autenticação de Dois Fatores",
|
||||
"adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.",
|
||||
"continueToApplication": "Continuar para o aplicativo",
|
||||
"securityKeyAdd": "Adicionar Chave de Segurança",
|
||||
"securityKeyRegisterTitle": "Registrar Nova Chave de Segurança",
|
||||
"securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Proxy Externo Habilitado",
|
||||
"addNewTarget": "Adicionar Novo Alvo",
|
||||
"targetsList": "Lista de Alvos",
|
||||
"advancedMode": "Modo Avançado",
|
||||
"targetErrorDuplicateTargetFound": "Alvo duplicado encontrado",
|
||||
"healthCheckHealthy": "Saudável",
|
||||
"healthCheckUnhealthy": "Não Saudável",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Erro de Login Automático",
|
||||
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
||||
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gerenciar Auto-Hospedados",
|
||||
"remoteExitNodeDescription": "Gerencie os nós para estender sua conectividade de rede",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Nós remotos",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Nós",
|
||||
"searchRemoteExitNodes": "Buscar nós...",
|
||||
"remoteExitNodeAdd": "Adicionar node",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.",
|
||||
"remoteExitNodeConfirmDelete": "Confirmar exclusão do nó",
|
||||
"remoteExitNodeDelete": "Excluir nó",
|
||||
"sidebarRemoteExitNodes": "Nós",
|
||||
"sidebarRemoteExitNodes": "Nós remotos",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Criar nó",
|
||||
"description": "Crie um novo nó para estender sua conectividade de rede",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Página de autenticação atualizada com sucesso",
|
||||
"healthCheckNotAvailable": "Localização",
|
||||
"rewritePath": "Reescrever Caminho",
|
||||
"rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino."
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
||||
"siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Просмотреть все сайты",
|
||||
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
||||
"siteNewtCredentials": "Учётные данные Newt",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Создано в",
|
||||
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
|
||||
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
|
||||
"proxyEnableSSL": "Включить SSL (https)",
|
||||
"proxyEnableSSL": "Включить SSL",
|
||||
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Настроить адресаты",
|
||||
"targetErrorFetch": "Не удалось получить цели",
|
||||
"targetErrorFetchDescription": "Произошла ошибка при получении целей",
|
||||
"siteErrorFetch": "Не удалось получить ресурс",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "Конфигурация безопасного соединения",
|
||||
"targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса",
|
||||
"targetTlsSettingsAdvanced": "Расширенные настройки TLS",
|
||||
"targetTlsSni": "Имя TLS сервера (SNI)",
|
||||
"targetTlsSni": "Имя TLS сервера",
|
||||
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
||||
"targetTlsSubmit": "Сохранить настройки",
|
||||
"targets": "Конфигурация целей",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
|
||||
"methodSelect": "Выберите метод",
|
||||
"targetSubmit": "Добавить цель",
|
||||
"targetNoOne": "Нет целей. Добавьте цель с помощью формы.",
|
||||
"targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы к вашему бэкэнду.",
|
||||
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
||||
"targetsSubmit": "Сохранить цели",
|
||||
"addTarget": "Добавить цель",
|
||||
"targetErrorInvalidIp": "Неверный IP-адрес",
|
||||
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
||||
"targetErrorInvalidPort": "Неверный порт",
|
||||
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
|
||||
"targetErrorNoSite": "Сайт не выбран",
|
||||
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
|
||||
"targetCreated": "Цель создана",
|
||||
"targetCreatedDescription": "Цель была успешно создана",
|
||||
"targetErrorCreate": "Не удалось создать цель",
|
||||
"targetErrorCreateDescription": "Произошла ошибка при создании цели",
|
||||
"save": "Сохранить",
|
||||
"proxyAdditional": "Дополнительные настройки прокси",
|
||||
"proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси",
|
||||
"proxyCustomHeader": "Пользовательский заголовок Host",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Администратор сервера - Pangolin",
|
||||
"licenseTierProfessional": "Профессиональная лицензия",
|
||||
"licenseTierEnterprise": "Корпоративная лицензия",
|
||||
"licenseTierCommercial": "Коммерческая лицензия",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Лицензировано",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
|
||||
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
|
||||
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
|
||||
"licenseBadge": "Профессиональная",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Тип поставщика",
|
||||
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
|
||||
"idpOidcConfigure": "Конфигурация OAuth2/OIDC",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Навигационное меню",
|
||||
"navbarDescription": "Главное навигационное меню приложения",
|
||||
"navbarDocsLink": "Документация",
|
||||
"commercialEdition": "Коммерческая версия",
|
||||
"otpErrorEnable": "Невозможно включить 2FA",
|
||||
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
|
||||
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Все пользователи",
|
||||
"sidebarIdentityProviders": "Поставщики удостоверений",
|
||||
"sidebarLicense": "Лицензия",
|
||||
"sidebarClients": "Клиенты (бета)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Домены",
|
||||
"enableDockerSocket": "Включить чертёж Docker",
|
||||
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Бесплатный уровень",
|
||||
"billingWarningOverLimit": "Предупреждение: Вы превысили одну или несколько границ использования. Ваши сайты не подключатся, пока вы не измените подписку или не скорректируете использование.",
|
||||
"billingUsageLimitsOverview": "Обзор лимитов использования",
|
||||
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@fossorial.io.",
|
||||
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@pangolin.net.",
|
||||
"billingDataUsage": "Использование данных",
|
||||
"billingOnlineTime": "Время работы сайта",
|
||||
"billingUsers": "Активные пользователи",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
|
||||
"twoFactor": "Двухфакторная аутентификация",
|
||||
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
|
||||
"continueToApplication": "Перейти к приложению",
|
||||
"securityKeyAdd": "Добавить ключ безопасности",
|
||||
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
|
||||
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Внешний прокси включен",
|
||||
"addNewTarget": "Добавить новую цель",
|
||||
"targetsList": "Список целей",
|
||||
"advancedMode": "Расширенный режим",
|
||||
"targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель",
|
||||
"healthCheckHealthy": "Здоровый",
|
||||
"healthCheckUnhealthy": "Нездоровый",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Ошибка автоматического входа",
|
||||
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Управление самоуправляемым",
|
||||
"remoteExitNodeDescription": "Управляйте узлами для расширения сетевого подключения",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Удаленные узлы",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Узлы",
|
||||
"searchRemoteExitNodes": "Поиск узлов...",
|
||||
"remoteExitNodeAdd": "Добавить узел",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.",
|
||||
"remoteExitNodeConfirmDelete": "Подтвердите удаление узла",
|
||||
"remoteExitNodeDelete": "Удалить узел",
|
||||
"sidebarRemoteExitNodes": "Узлы",
|
||||
"sidebarRemoteExitNodes": "Удаленные узлы",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Создать узел",
|
||||
"description": "Создайте новый узел, чтобы расширить сетевое подключение",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Страница авторизации успешно обновлена",
|
||||
"healthCheckNotAvailable": "Локальный",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
||||
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "Tüm Siteleri Gör",
|
||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "Oluşturulma Tarihi",
|
||||
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
|
||||
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
|
||||
"proxyEnableSSL": "SSL'yi Etkinleştir (https)",
|
||||
"proxyEnableSSL": "SSL Etkinleştir",
|
||||
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
|
||||
"target": "Hedef",
|
||||
"configureTarget": "Hedefleri Yapılandır",
|
||||
"targetErrorFetch": "Hedefleri alamadı",
|
||||
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
|
||||
"siteErrorFetch": "kaynağa ulaşılamadı",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "HTTPS & TLS Settings",
|
||||
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
|
||||
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
|
||||
"targetTlsSni": "TLS Sunucu Adı (SNI)",
|
||||
"targetTlsSni": "TLS Sunucu Adı",
|
||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||
"targetTlsSubmit": "Ayarları Kaydet",
|
||||
"targets": "Hedefler Konfigürasyonu",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||
"methodSelect": "Yöntemi Seç",
|
||||
"targetSubmit": "Hedef Ekle",
|
||||
"targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir hedef ekleyin.",
|
||||
"targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
|
||||
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
||||
"targetsSubmit": "Hedefleri Kaydet",
|
||||
"addTarget": "Hedef Ekle",
|
||||
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
||||
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
||||
"targetErrorInvalidPort": "Geçersiz port",
|
||||
"targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin",
|
||||
"targetErrorNoSite": "Hiçbir site seçili değil",
|
||||
"targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin",
|
||||
"targetCreated": "Hedef oluşturuldu",
|
||||
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
|
||||
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
|
||||
"targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu",
|
||||
"save": "Kaydet",
|
||||
"proxyAdditional": "Ek Proxy Ayarları",
|
||||
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
|
||||
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
||||
"licenseTierProfessional": "Profesyonel Lisans",
|
||||
"licenseTierEnterprise": "Kurumsal Lisans",
|
||||
"licenseTierCommercial": "Ticari Lisans",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "Lisanslı",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
|
||||
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
|
||||
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
|
||||
"licenseBadge": "Profesyonel",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Sağlayıcı Türü",
|
||||
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Yapılandırması",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "Navigasyon Menüsü",
|
||||
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
|
||||
"navbarDocsLink": "Dokümantasyon",
|
||||
"commercialEdition": "Ticari Sürüm",
|
||||
"otpErrorEnable": "2FA etkinleştirilemedi",
|
||||
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
|
||||
"otpSetupCheckCode": "6 haneli bir kod girin",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "Tüm Kullanıcılar",
|
||||
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
|
||||
"sidebarLicense": "Lisans",
|
||||
"sidebarClients": "Müşteriler (Beta)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "Alan Adları",
|
||||
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
||||
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "Ücretsiz Dilim",
|
||||
"billingWarningOverLimit": "Uyarı: Bir veya daha fazla kullanım limitini aştınız. Aboneliğinizi değiştirmediğiniz veya kullanımı ayarlamadığınız sürece siteleriniz bağlanmayacaktır.",
|
||||
"billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü",
|
||||
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@fossorial.io adresinden bizimle iletişime geçin.",
|
||||
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@pangolin.net adresinden bizimle iletişime geçin.",
|
||||
"billingDataUsage": "Veri Kullanımı",
|
||||
"billingOnlineTime": "Site Çevrimiçi Süresi",
|
||||
"billingUsers": "Aktif Kullanıcılar",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
|
||||
"twoFactor": "İki Faktörlü Kimlik Doğrulama",
|
||||
"adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.",
|
||||
"continueToApplication": "Uygulamaya Devam Et",
|
||||
"securityKeyAdd": "Güvenlik Anahtarı Ekle",
|
||||
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
|
||||
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "Dış Proxy Etkinleştirildi",
|
||||
"addNewTarget": "Yeni Hedef Ekle",
|
||||
"targetsList": "Hedefler Listesi",
|
||||
"advancedMode": "Gelişmiş Mod",
|
||||
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
|
||||
"healthCheckHealthy": "Sağlıklı",
|
||||
"healthCheckUnhealthy": "Sağlıksız",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "Otomatik Giriş Hatası",
|
||||
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Öz-Host Yönetim",
|
||||
"remoteExitNodeDescription": "Ağ bağlantınızı genişletmek için düğümleri yönetin",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "Düğümler",
|
||||
"searchRemoteExitNodes": "Düğüm ara...",
|
||||
"remoteExitNodeAdd": "Düğüm Ekle",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.",
|
||||
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
|
||||
"remoteExitNodeDelete": "Düğümü Sil",
|
||||
"sidebarRemoteExitNodes": "Düğümler",
|
||||
"sidebarRemoteExitNodes": "Uzak Düğümler",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Düğüm Oluştur",
|
||||
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
|
||||
"healthCheckNotAvailable": "Yerel",
|
||||
"rewritePath": "Yolu Yeniden Yaz",
|
||||
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın."
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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。",
|
||||
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
||||
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
||||
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteSeeAll": "查看所有站点",
|
||||
"siteTunnelDescription": "确定如何连接到您的网站",
|
||||
"siteNewtCredentials": "Newt 凭据",
|
||||
@@ -468,7 +468,10 @@
|
||||
"createdAt": "创建于",
|
||||
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
|
||||
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
|
||||
"proxyEnableSSL": "启用 SSL (https)",
|
||||
"proxyEnableSSL": "启用 SSL",
|
||||
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。",
|
||||
"target": "Target",
|
||||
"configureTarget": "配置目标",
|
||||
"targetErrorFetch": "获取目标失败",
|
||||
"targetErrorFetchDescription": "获取目标时出错",
|
||||
"siteErrorFetch": "获取资源失败",
|
||||
@@ -495,7 +498,7 @@
|
||||
"targetTlsSettings": "安全连接配置",
|
||||
"targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置",
|
||||
"targetTlsSettingsAdvanced": "高级TLS设置",
|
||||
"targetTlsSni": "TLS 服务器名称 (SNI)",
|
||||
"targetTlsSni": "TLS 服务器名称",
|
||||
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
||||
"targetTlsSubmit": "保存设置",
|
||||
"targets": "目标配置",
|
||||
@@ -504,9 +507,21 @@
|
||||
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
||||
"methodSelect": "选择方法",
|
||||
"targetSubmit": "添加目标",
|
||||
"targetNoOne": "没有目标。使用表单添加目标。",
|
||||
"targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。",
|
||||
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
||||
"targetsSubmit": "保存目标",
|
||||
"addTarget": "添加目标",
|
||||
"targetErrorInvalidIp": "无效的 IP 地址",
|
||||
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
||||
"targetErrorInvalidPort": "无效的端口",
|
||||
"targetErrorInvalidPortDescription": "请输入有效的端口号",
|
||||
"targetErrorNoSite": "没有选择站点",
|
||||
"targetErrorNoSiteDescription": "请选择目标站点",
|
||||
"targetCreated": "目标已创建",
|
||||
"targetCreatedDescription": "目标已成功创建",
|
||||
"targetErrorCreate": "创建目标失败",
|
||||
"targetErrorCreateDescription": "创建目标时出错",
|
||||
"save": "保存",
|
||||
"proxyAdditional": "附加代理设置",
|
||||
"proxyAdditionalDescription": "配置你的资源如何处理代理设置",
|
||||
"proxyCustomHeader": "自定义主机标题",
|
||||
@@ -715,7 +730,7 @@
|
||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||
"licenseTierProfessional": "专业许可证",
|
||||
"licenseTierEnterprise": "企业许可证",
|
||||
"licenseTierCommercial": "商业许可证",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licensed": "已授权",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
@@ -750,7 +765,7 @@
|
||||
"idpDisplayName": "此身份提供商的显示名称",
|
||||
"idpAutoProvisionUsers": "自动提供用户",
|
||||
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
||||
"licenseBadge": "专业版",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "提供者类型",
|
||||
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 配置",
|
||||
@@ -1084,7 +1099,6 @@
|
||||
"navbar": "导航菜单",
|
||||
"navbarDescription": "应用程序的主导航菜单",
|
||||
"navbarDocsLink": "文件",
|
||||
"commercialEdition": "商业版",
|
||||
"otpErrorEnable": "无法启用 2FA",
|
||||
"otpErrorEnableDescription": "启用 2FA 时出错",
|
||||
"otpSetupCheckCode": "请输入您的6位数字代码",
|
||||
@@ -1140,7 +1154,7 @@
|
||||
"sidebarAllUsers": "所有用户",
|
||||
"sidebarIdentityProviders": "身份提供商",
|
||||
"sidebarLicense": "证书",
|
||||
"sidebarClients": "客户端(测试版)",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarDomains": "域",
|
||||
"enableDockerSocket": "启用 Docker 蓝图",
|
||||
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
||||
@@ -1266,7 +1280,7 @@
|
||||
"billingFreeTier": "免费层",
|
||||
"billingWarningOverLimit": "警告:您已超出一个或多个使用限制。在您修改订阅或调整使用情况之前,您的站点将无法连接。",
|
||||
"billingUsageLimitsOverview": "使用限制概览",
|
||||
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@fossorial.io。",
|
||||
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@pangolin.net。",
|
||||
"billingDataUsage": "数据使用情况",
|
||||
"billingOnlineTime": "站点在线时间",
|
||||
"billingUsers": "活跃用户",
|
||||
@@ -1333,7 +1347,6 @@
|
||||
"twoFactorRequired": "注册安全密钥需要两步验证。",
|
||||
"twoFactor": "两步验证",
|
||||
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
|
||||
"continueToApplication": "继续应用",
|
||||
"securityKeyAdd": "添加安全密钥",
|
||||
"securityKeyRegisterTitle": "注册新安全密钥",
|
||||
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"externalProxyEnabled": "外部代理已启用",
|
||||
"addNewTarget": "添加新目标",
|
||||
"targetsList": "目标列表",
|
||||
"advancedMode": "高级模式",
|
||||
"targetErrorDuplicateTargetFound": "找到重复的目标",
|
||||
"healthCheckHealthy": "正常",
|
||||
"healthCheckUnhealthy": "不正常",
|
||||
@@ -1543,8 +1557,8 @@
|
||||
"autoLoginError": "自动登录错误",
|
||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
||||
"remoteExitNodeManageRemoteExitNodes": "管理自托管",
|
||||
"remoteExitNodeDescription": "管理节点以扩展您的网络连接",
|
||||
"remoteExitNodeManageRemoteExitNodes": "远程节点",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodes": "节点",
|
||||
"searchRemoteExitNodes": "搜索节点...",
|
||||
"remoteExitNodeAdd": "添加节点",
|
||||
@@ -1554,7 +1568,7 @@
|
||||
"remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。",
|
||||
"remoteExitNodeConfirmDelete": "确认删除节点",
|
||||
"remoteExitNodeDelete": "删除节点",
|
||||
"sidebarRemoteExitNodes": "节点",
|
||||
"sidebarRemoteExitNodes": "远程节点",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "创建节点",
|
||||
"description": "创建一个新节点来扩展您的网络连接",
|
||||
@@ -1723,5 +1737,161 @@
|
||||
"authPageUpdated": "身份验证页面更新成功",
|
||||
"healthCheckNotAvailable": "本地的",
|
||||
"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@pangolin.net."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
5749
package-lock.json
generated
5749
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -19,21 +19,21 @@
|
||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||
"db:clear-migrations": "rm -rf server/migrations",
|
||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts",
|
||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts",
|
||||
"set:enterprise": "echo 'export const build = \"enterprise\" 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 && cp tsconfig.saas.json tsconfig.json",
|
||||
"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:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
|
||||
"next:build": "next build",
|
||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
||||
"db: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);\""
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
@@ -56,11 +56,11 @@
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "0.5.5",
|
||||
"@react-email/render": "^1.2.0",
|
||||
"@react-email/components": "0.5.7",
|
||||
"@react-email/render": "^1.3.2",
|
||||
"@react-email/tailwind": "1.2.2",
|
||||
"@simplewebauthn/browser": "^13.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"arctic": "^3.7.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"drizzle-orm": "0.44.6",
|
||||
"eslint": "9.35.0",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-config-next": "15.5.6",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "8.1.0",
|
||||
"glob": "11.0.3",
|
||||
@@ -85,40 +85,40 @@
|
||||
"http-errors": "2.0.0",
|
||||
"i": "^0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.6.1",
|
||||
"ioredis": "5.8.1",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"maxmind": "5.0.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.4",
|
||||
"next-intl": "^4.3.9",
|
||||
"next": "15.5.6",
|
||||
"next-intl": "^4.3.12",
|
||||
"next-themes": "0.4.6",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.6",
|
||||
"npm": "^11.6.1",
|
||||
"nodemailer": "7.0.9",
|
||||
"npm": "^11.6.2",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.8.4",
|
||||
"posthog-node": "^5.9.5",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-easy-sort": "^1.7.0",
|
||||
"react-hook-form": "7.62.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-easy-sort": "^1.8.0",
|
||||
"react-hook-form": "7.65.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"reodotdev": "^1.0.0",
|
||||
"resend": "^6.1.1",
|
||||
"semver": "^7.7.2",
|
||||
"resend": "^6.1.2",
|
||||
"semver": "^7.7.3",
|
||||
"stripe": "18.2.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"uuid": "^13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"winston": "3.17.0",
|
||||
"winston": "3.18.3",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yargs": "18.0.0",
|
||||
@@ -128,7 +128,7 @@
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.51.0",
|
||||
"@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",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/cookie-parser": "1.4.9",
|
||||
@@ -139,25 +139,25 @@
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "24.6.2",
|
||||
"@types/node": "24.7.2",
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"@types/pg": "8.15.5",
|
||||
"@types/react": "19.1.16",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.31.5",
|
||||
"esbuild": "0.25.10",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.2.12",
|
||||
"react-email": "4.3.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.45.0"
|
||||
"typescript-eslint": "^8.46.0"
|
||||
},
|
||||
"overrides": {
|
||||
"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,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage";
|
||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||
import helmet from "helmet";
|
||||
import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks";
|
||||
import { build } from "./build";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "./types/HttpCode";
|
||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||
import { createStore } from "@server/lib/private/rateLimitStore";
|
||||
import hybridRouter from "@server/routers/private/hybrid";
|
||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||
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 externalPort = config.getRawConfig().server.external_port;
|
||||
@@ -39,32 +39,30 @@ export function createApiServer() {
|
||||
apiServer.post(
|
||||
`${prefix}/billing/webhooks`,
|
||||
express.raw({ type: "application/json" }),
|
||||
stripeWebhookHandler
|
||||
billingWebhookHandler
|
||||
);
|
||||
}
|
||||
|
||||
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") {
|
||||
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" || !corsConfig) {
|
||||
logger.debug("Using CORS options", options);
|
||||
|
||||
apiServer.use(cors(options));
|
||||
} else {
|
||||
} else if (corsConfig) {
|
||||
// Use the custom CORS middleware with loginPage support
|
||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { sendUsageNotification } from "@server/routers/org";
|
||||
|
||||
export enum ActionsEnum {
|
||||
createOrgUser = "createOrgUser",
|
||||
@@ -61,6 +60,7 @@ export enum ActionsEnum {
|
||||
getUser = "getUser",
|
||||
setResourcePassword = "setResourcePassword",
|
||||
setResourcePincode = "setResourcePincode",
|
||||
setResourceHeaderAuth = "setResourceHeaderAuth",
|
||||
setResourceWhitelist = "setResourceWhitelist",
|
||||
getResourceWhitelist = "getResourceWhitelist",
|
||||
generateAccessToken = "generateAccessToken",
|
||||
@@ -194,7 +194,6 @@ export async function checkUserActionPermission(
|
||||
|
||||
return roleActionPermission.length > 0;
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking user action permission:", error);
|
||||
throw createHttpError(
|
||||
|
||||
@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
@@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
|
||||
token: string,
|
||||
resourceId: number
|
||||
): Promise<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(
|
||||
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
|
||||
const poolConfig = config.postgres.pool;
|
||||
const primaryPool = new Pool({
|
||||
connectionString,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
max: poolConfig?.max_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||
});
|
||||
|
||||
const replicas = [];
|
||||
@@ -50,9 +51,9 @@ function createDb() {
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = new Pool({
|
||||
connectionString: conn.connection_string,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
max: poolConfig?.max_replica_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||
});
|
||||
replicas.push(DrizzlePostgres(replicaPool));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
export * from "./privateSchema";
|
||||
export * from "./schema/schema";
|
||||
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 {
|
||||
pgTable,
|
||||
serial,
|
||||
@@ -126,7 +126,7 @@ export const targets = pgTable("targets", {
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||
priority: integer("priority").notNull().default(100)
|
||||
priority: integer("priority").default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
@@ -381,6 +381,14 @@ export const resourcePassword = pgTable("resourcePassword", {
|
||||
passwordHash: varchar("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
||||
headerAuthId: serial("headerAuthId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
@@ -466,8 +474,6 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateRuleId: integer("templateRuleId")
|
||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
||||
@@ -475,40 +481,6 @@ export const resourceRules = pgTable("resourceRules", {
|
||||
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", {
|
||||
keyId: serial("keyId").primaryKey(),
|
||||
key: varchar("key").notNull(),
|
||||
@@ -726,6 +698,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
@@ -748,6 +721,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ResourceRule,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceHeaderAuth,
|
||||
ResourceHeaderAuth,
|
||||
resourceRules,
|
||||
resources,
|
||||
roleResources,
|
||||
@@ -15,15 +17,12 @@ import {
|
||||
users
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import axios from "axios";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export type ResourceWithAuth = {
|
||||
resource: Resource | null;
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
@@ -37,30 +36,6 @@ export type UserSessionWithUser = {
|
||||
export async function getResourceByDomain(
|
||||
domain: string
|
||||
): Promise<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
|
||||
.select()
|
||||
.from(resources)
|
||||
@@ -72,6 +47,10 @@ export async function getResourceByDomain(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -82,7 +61,8 @@ export async function getResourceByDomain(
|
||||
return {
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,30 +72,6 @@ export async function getResourceByDomain(
|
||||
export async function getUserSessionWithUser(
|
||||
userSessionId: string
|
||||
): Promise<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
|
||||
.select()
|
||||
.from(sessions)
|
||||
@@ -136,30 +92,6 @@ export async function getUserSessionWithUser(
|
||||
* Get user organization role
|
||||
*/
|
||||
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
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
@@ -176,30 +108,6 @@ export async function getRoleResourceAccess(
|
||||
resourceId: 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
|
||||
.select()
|
||||
.from(roleResources)
|
||||
@@ -221,30 +129,6 @@ export async function getUserResourceAccess(
|
||||
userId: string,
|
||||
resourceId: number
|
||||
) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
|
||||
await tokenManager.getAuthHeader()
|
||||
);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
@@ -265,30 +149,6 @@ export async function getUserResourceAccess(
|
||||
export async function getResourceRules(
|
||||
resourceId: number
|
||||
): 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
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
@@ -303,30 +163,6 @@ export async function getResourceRules(
|
||||
export async function getOrgLoginPage(
|
||||
orgId: string
|
||||
): 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
|
||||
.select()
|
||||
.from(loginPageOrg)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import * as schema from "./schema";
|
||||
import * as schema from "./schema/schema";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema";
|
||||
export * from "./privateSchema";
|
||||
export * from "./schema/schema";
|
||||
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 {
|
||||
sqliteTable,
|
||||
integer,
|
||||
@@ -138,7 +138,7 @@ export const targets = sqliteTable("targets", {
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||
priority: integer("priority").notNull().default(100)
|
||||
priority: integer("priority").default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
@@ -514,6 +514,16 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
||||
passwordHash: text("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
||||
headerAuthId: integer("headerAuthId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
headerAuthHash: text("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||
accessTokenId: text("accessTokenId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
@@ -600,8 +610,6 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
templateRuleId: integer("templateRuleId")
|
||||
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
||||
@@ -609,40 +617,6 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
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", {
|
||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||
key: text("key").notNull(),
|
||||
@@ -765,6 +739,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
@@ -785,6 +760,4 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
@@ -6,11 +6,6 @@ import logger from "@server/logger";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
function createEmailClient() {
|
||||
if (config.isManagedMode()) {
|
||||
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
||||
return;
|
||||
}
|
||||
|
||||
const emailConfig = config.getRawConfig().email;
|
||||
if (!emailConfig) {
|
||||
logger.warn(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { render } from "@react-email/render";
|
||||
import { ReactElement } from "react";
|
||||
import emailClient from "@server/emails";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export async function sendEmail(
|
||||
template: ReactElement,
|
||||
@@ -25,7 +24,7 @@ export async function sendEmail(
|
||||
|
||||
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({
|
||||
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 { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
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 { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
@@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({
|
||||
To learn how to use Newt, including more
|
||||
installation methods, visit the{" "}
|
||||
<a
|
||||
href="https://docs.digpangolin.com/manage/sites/install-site"
|
||||
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||
className="underline"
|
||||
>
|
||||
docs
|
||||
|
||||
@@ -89,7 +89,7 @@ export function EmailFooter({ children }: { children: React.ReactNode }) {
|
||||
<p className="text-xs text-gray-400 mt-4">
|
||||
For any questions or support, please contact us at:
|
||||
<br />
|
||||
support@fossorial.io
|
||||
support@pangolin.net
|
||||
</p>
|
||||
<p className="text-xs text-gray-300 text-center mt-4">
|
||||
© {new Date().getFullYear()} Fossorial, Inc. All
|
||||
|
||||
@@ -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 { createNextServer } from "./nextServer";
|
||||
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 { createHybridClientServer } from "./hybridServer";
|
||||
import config from "@server/lib/config";
|
||||
import { setHostMeta } from "@server/lib/hostMeta";
|
||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
|
||||
async function startServers() {
|
||||
await setHostMeta();
|
||||
|
||||
await config.initServer();
|
||||
|
||||
license.setServerSecret(config.getRawConfig().server.secret!);
|
||||
await license.check();
|
||||
|
||||
await runSetupFunctions();
|
||||
|
||||
initTelemetryClient();
|
||||
@@ -25,16 +37,11 @@ async function startServers() {
|
||||
const apiServer = createApiServer();
|
||||
const internalServer = createInternalServer();
|
||||
|
||||
let hybridClientServer;
|
||||
let nextServer;
|
||||
if (config.isManagedMode()) {
|
||||
hybridClientServer = await createHybridClientServer();
|
||||
} else {
|
||||
nextServer = await createNextServer();
|
||||
if (config.getRawConfig().traefik.file_mode) {
|
||||
const monitor = new TraefikConfigManager();
|
||||
await monitor.start();
|
||||
}
|
||||
nextServer = await createNextServer();
|
||||
if (config.getRawConfig().traefik.file_mode) {
|
||||
const monitor = new TraefikConfigManager();
|
||||
await monitor.start();
|
||||
}
|
||||
|
||||
let integrationServer;
|
||||
@@ -42,12 +49,13 @@ async function startServers() {
|
||||
integrationServer = createIntegrationApiServer();
|
||||
}
|
||||
|
||||
await initCleanup();
|
||||
|
||||
return {
|
||||
apiServer,
|
||||
nextServer,
|
||||
internalServer,
|
||||
integrationServer,
|
||||
hybridClientServer
|
||||
integrationServer
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware,
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "@server/routers/integration";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
import helmet from "helmet";
|
||||
import swaggerUi from "swagger-ui-express";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import internal from "@server/routers/internal";
|
||||
import { internalRouter } from "#dynamic/routers/internal";
|
||||
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
|
||||
|
||||
const internalPort = config.getRawConfig().server.internal_port;
|
||||
@@ -23,7 +23,7 @@ export function createInternalServer() {
|
||||
internalServer.use(express.json());
|
||||
|
||||
const prefix = `/api/v1`;
|
||||
internalServer.use(prefix, internal);
|
||||
internalServer.use(prefix, internalRouter);
|
||||
|
||||
internalServer.use(notFoundMiddleware);
|
||||
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";
|
||||
|
||||
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";
|
||||
|
||||
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 { and, eq } from "drizzle-orm";
|
||||
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 {
|
||||
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 NodeCache from "node-cache";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { s3Client } from "../s3";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import {
|
||||
@@ -30,10 +16,10 @@ import {
|
||||
Transaction
|
||||
} from "@server/db";
|
||||
import { FeatureId, getFeatureMeterId } from "./features";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { sendToClient } from "@server/routers/ws";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { build } from "@server/build";
|
||||
import { s3Client } from "@server/lib/s3";
|
||||
|
||||
interface StripeEvent {
|
||||
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 {
|
||||
private cache: NodeCache;
|
||||
private bucketName: string | undefined;
|
||||
@@ -55,11 +52,13 @@ export class UsageService {
|
||||
|
||||
constructor() {
|
||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
||||
if (build !== "saas") {
|
||||
if (noop()) {
|
||||
return;
|
||||
}
|
||||
this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket;
|
||||
this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath;
|
||||
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
||||
// 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
|
||||
this.initializeEventsDirectory().then(() => {
|
||||
@@ -83,7 +82,9 @@ export class UsageService {
|
||||
|
||||
private async initializeEventsDirectory(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
@@ -95,7 +96,9 @@ export class UsageService {
|
||||
|
||||
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
@@ -118,15 +121,17 @@ export class UsageService {
|
||||
ContentType: "application/json"
|
||||
});
|
||||
await s3Client.send(uploadCommand);
|
||||
|
||||
|
||||
// Check if file still exists before unlinking
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
logger.debug(`Startup file ${file} was already deleted`);
|
||||
logger.debug(
|
||||
`Startup file ${file} was already deleted`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
logger.info(
|
||||
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
|
||||
);
|
||||
@@ -136,7 +141,9 @@ export class UsageService {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
logger.debug(`Empty startup file ${file} was already deleted`);
|
||||
logger.debug(
|
||||
`Empty startup file ${file} was already deleted`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -147,8 +154,8 @@ export class UsageService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to scan for leftover event files:", err);
|
||||
} catch (error) {
|
||||
logger.error("Failed to scan for leftover event files");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,17 +165,17 @@ export class UsageService {
|
||||
value: number,
|
||||
transaction: any = null
|
||||
): Promise<Usage | null> {
|
||||
if (build !== "saas") {
|
||||
if (noop()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Truncate value to 11 decimal places
|
||||
value = this.truncateValue(value);
|
||||
|
||||
|
||||
// Implement retry logic for deadlock handling
|
||||
const maxRetries = 3;
|
||||
let attempt = 0;
|
||||
|
||||
|
||||
while (attempt <= maxRetries) {
|
||||
try {
|
||||
// Get subscription data for this org (with caching)
|
||||
@@ -191,7 +198,12 @@ export class UsageService {
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
} catch (error: any) {
|
||||
// Check if this is a deadlock error
|
||||
const isDeadlock = error?.code === '40P01' ||
|
||||
error?.cause?.code === '40P01' ||
|
||||
(error?.message && error.message.includes('deadlock'));
|
||||
|
||||
const isDeadlock =
|
||||
error?.code === "40P01" ||
|
||||
error?.cause?.code === "40P01" ||
|
||||
(error?.message && error.message.includes("deadlock"));
|
||||
|
||||
if (isDeadlock && attempt < maxRetries) {
|
||||
attempt++;
|
||||
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
|
||||
const baseDelay = Math.pow(2, attempt - 1) * 50;
|
||||
const jitter = Math.random() * baseDelay;
|
||||
const delay = baseDelay + jitter;
|
||||
|
||||
|
||||
logger.warn(
|
||||
`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;
|
||||
}
|
||||
|
||||
|
||||
logger.error(
|
||||
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
|
||||
error
|
||||
@@ -239,10 +252,10 @@ export class UsageService {
|
||||
): Promise<Usage> {
|
||||
// Truncate value to 11 decimal places
|
||||
value = this.truncateValue(value);
|
||||
|
||||
|
||||
const usageId = `${orgId}-${featureId}`;
|
||||
const meterId = getFeatureMeterId(featureId);
|
||||
|
||||
|
||||
// Use upsert: insert if not exists, otherwise increment
|
||||
const [returnUsage] = await trx
|
||||
.insert(usage)
|
||||
@@ -259,7 +272,8 @@ export class UsageService {
|
||||
set: {
|
||||
latestValue: sql`${usage.latestValue} + ${value}`
|
||||
}
|
||||
}).returning();
|
||||
})
|
||||
.returning();
|
||||
|
||||
return returnUsage;
|
||||
}
|
||||
@@ -280,7 +294,7 @@ export class UsageService {
|
||||
value?: number,
|
||||
customerId?: string
|
||||
): Promise<void> {
|
||||
if (build !== "saas") {
|
||||
if (noop()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -351,7 +365,7 @@ export class UsageService {
|
||||
.set({
|
||||
latestValue: newRunningTotal,
|
||||
instantaneousValue: value,
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(usage.usageId, usageId));
|
||||
}
|
||||
@@ -366,7 +380,7 @@ export class UsageService {
|
||||
meterId,
|
||||
instantaneousValue: truncatedValue,
|
||||
latestValue: truncatedValue,
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -427,7 +441,7 @@ export class UsageService {
|
||||
): Promise<void> {
|
||||
// Truncate value to 11 decimal places before sending to Stripe
|
||||
const truncatedValue = this.truncateValue(value);
|
||||
|
||||
|
||||
const event: StripeEvent = {
|
||||
identifier: uuidv4(),
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
@@ -444,7 +458,9 @@ export class UsageService {
|
||||
|
||||
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
||||
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;
|
||||
}
|
||||
if (!this.currentEventFile) {
|
||||
@@ -493,7 +509,9 @@ export class UsageService {
|
||||
|
||||
private async uploadFileToS3(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
if (!this.currentEventFile) {
|
||||
@@ -505,7 +523,9 @@ export class UsageService {
|
||||
|
||||
// Check if this file is already being uploaded
|
||||
if (this.uploadingFiles.has(fileName)) {
|
||||
logger.debug(`File ${fileName} is already being uploaded, skipping`);
|
||||
logger.debug(
|
||||
`File ${fileName} is already being uploaded, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -517,7 +537,9 @@ export class UsageService {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} 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);
|
||||
// Reset current file if it was this file
|
||||
if (this.currentEventFile === fileName) {
|
||||
@@ -537,7 +559,9 @@ export class UsageService {
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
// 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.uploadingFiles.delete(fileName);
|
||||
@@ -560,7 +584,9 @@ export class UsageService {
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
// 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(
|
||||
@@ -571,10 +597,7 @@ export class UsageService {
|
||||
this.currentEventFile = null;
|
||||
this.currentFileStartTime = 0;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to upload ${fileName} to S3:`,
|
||||
error
|
||||
);
|
||||
logger.error(`Failed to upload ${fileName} to S3:`, error);
|
||||
} finally {
|
||||
// Always remove from uploading set
|
||||
this.uploadingFiles.delete(fileName);
|
||||
@@ -589,16 +612,17 @@ export class UsageService {
|
||||
|
||||
public async getUsage(
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
featureId: FeatureId,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<Usage | null> {
|
||||
if (build !== "saas") {
|
||||
if (noop()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usageId = `${orgId}-${featureId}`;
|
||||
|
||||
try {
|
||||
const [result] = await db
|
||||
const [result] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -610,9 +634,9 @@ export class UsageService {
|
||||
`Creating new usage record for ${orgId}/${featureId}`
|
||||
);
|
||||
const meterId = getFeatureMeterId(featureId);
|
||||
|
||||
|
||||
try {
|
||||
const [newUsage] = await db
|
||||
const [newUsage] = await trx
|
||||
.insert(usage)
|
||||
.values({
|
||||
usageId,
|
||||
@@ -629,7 +653,7 @@ export class UsageService {
|
||||
return newUsage;
|
||||
} else {
|
||||
// Record was created by another process, fetch it
|
||||
const [existingUsage] = await db
|
||||
const [existingUsage] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -642,7 +666,7 @@ export class UsageService {
|
||||
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
|
||||
insertError
|
||||
);
|
||||
const [existingUsage] = await db
|
||||
const [existingUsage] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -665,7 +689,7 @@ export class UsageService {
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
): Promise<Usage | null> {
|
||||
if (build !== "saas") {
|
||||
if (noop()) {
|
||||
return null;
|
||||
}
|
||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||
@@ -685,7 +709,9 @@ export class UsageService {
|
||||
*/
|
||||
private async uploadOldEventFiles(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
@@ -693,15 +719,17 @@ export class UsageService {
|
||||
const now = Date.now();
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".json")) continue;
|
||||
|
||||
|
||||
// Skip files that are already being uploaded
|
||||
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;
|
||||
}
|
||||
|
||||
const filePath = path.join(this.eventsDir, file);
|
||||
|
||||
|
||||
try {
|
||||
// Check if file still exists before processing
|
||||
try {
|
||||
@@ -716,7 +744,7 @@ export class UsageService {
|
||||
if (age >= 90000) {
|
||||
// 1.5 minutes - Mark as being uploaded
|
||||
this.uploadingFiles.add(file);
|
||||
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(
|
||||
filePath,
|
||||
@@ -732,15 +760,17 @@ export class UsageService {
|
||||
ContentType: "application/json"
|
||||
});
|
||||
await s3Client.send(uploadCommand);
|
||||
|
||||
|
||||
// Check if file still exists before unlinking
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
logger.debug(`File ${file} was already deleted during interval upload`);
|
||||
logger.debug(
|
||||
`File ${file} was already deleted during interval upload`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
logger.info(
|
||||
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
|
||||
);
|
||||
@@ -755,7 +785,9 @@ export class UsageService {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
} catch (unlinkError) {
|
||||
logger.debug(`Empty file ${file} was already deleted`);
|
||||
logger.debug(
|
||||
`Empty file ${file} was already deleted`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -777,19 +809,25 @@ export class UsageService {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
||||
if (build !== "saas") {
|
||||
public async checkLimitSet(
|
||||
orgId: string,
|
||||
kickSites = false,
|
||||
featureId?: FeatureId,
|
||||
usage?: Usage,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<boolean> {
|
||||
if (noop()) {
|
||||
return false;
|
||||
}
|
||||
// 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;
|
||||
|
||||
try {
|
||||
let orgLimits: Limit[] = [];
|
||||
if (featureId) {
|
||||
// Get all limits set for this organization
|
||||
orgLimits = await db
|
||||
orgLimits = await trx
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(
|
||||
@@ -800,7 +838,7 @@ export class UsageService {
|
||||
);
|
||||
} else {
|
||||
// Get all limits set for this organization
|
||||
orgLimits = await db
|
||||
orgLimits = await trx
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(eq(limits.orgId, orgId));
|
||||
@@ -817,16 +855,31 @@ export class UsageService {
|
||||
if (usage) {
|
||||
currentUsage = usage;
|
||||
} else {
|
||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
||||
currentUsage = await this.getUsage(
|
||||
orgId,
|
||||
limit.featureId as FeatureId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 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) {
|
||||
const usageValue =
|
||||
currentUsage?.instantaneousValue ||
|
||||
currentUsage?.latestValue ||
|
||||
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(
|
||||
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
||||
`${usageValue} > ${limit.value}`
|
||||
`${usageValue} > ${limit.value}`
|
||||
);
|
||||
hasExceededLimits = true;
|
||||
break; // Exit early if any limit is exceeded
|
||||
@@ -835,22 +888,24 @@ export class UsageService {
|
||||
|
||||
// If any limits are exceeded, disconnect all sites for this organization
|
||||
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
|
||||
const orgSites = await db
|
||||
const orgSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
|
||||
// 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) {
|
||||
// Send termination messages to newt sites
|
||||
for (const site of orgSites) {
|
||||
if (site.type === "newt") {
|
||||
const [newt] = await db
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
@@ -865,17 +920,21 @@ export class UsageService {
|
||||
};
|
||||
|
||||
// Don't await to prevent blocking
|
||||
sendToClient(newt.newtId, payload).catch((error: any) => {
|
||||
logger.error(
|
||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
await sendToClient(newt.newtId, payload).catch(
|
||||
(error: any) => {
|
||||
logger.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) {
|
||||
@@ -139,8 +139,8 @@ export async function applyBlueprint(
|
||||
// password: "sadfasdfadsf",
|
||||
// "sso-enabled": true,
|
||||
// "sso-roles": ["Member"],
|
||||
// "sso-users": ["owen@fossorial.io"],
|
||||
// "whitelist-users": ["owen@fossorial.io"]
|
||||
// "sso-users": ["owen@pangolin.net"],
|
||||
// "whitelist-users": ["owen@pangolin.net"]
|
||||
// },
|
||||
// targets: [
|
||||
// {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sendToClient } from "@server/routers/ws";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { processContainerLabels } from "./parseDockerContainers";
|
||||
import { applyBlueprint } from "./applyBlueprint";
|
||||
import { db, sites } from "@server/db";
|
||||
|
||||
@@ -87,8 +87,8 @@ export function convertValue(value: string): any {
|
||||
// "resources.resource-nice-id.auth.password": "sadfasdfadsf",
|
||||
// "resources.resource-nice-id.auth.sso-enabled": "true",
|
||||
// "resources.resource-nice-id.auth.sso-roles[0]": "Member",
|
||||
// "resources.resource-nice-id.auth.sso-users[0]": "owen@fossorial.io",
|
||||
// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@fossorial.io",
|
||||
// "resources.resource-nice-id.auth.sso-users[0]": "owen@pangolin.net",
|
||||
// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@pangolin.net",
|
||||
// "resources.resource-nice-id.targets[0].hostname": "localhost",
|
||||
// "resources.resource-nice-id.targets[0].method": "http",
|
||||
// "resources.resource-nice-id.targets[0].port": "8000",
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
domains,
|
||||
orgDomains,
|
||||
Resource,
|
||||
resourceHeaderAuth,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resourceWhitelist,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
TargetData
|
||||
} from "./types";
|
||||
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 { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
@@ -123,7 +124,9 @@ export async function updateProxyResources(
|
||||
|
||||
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
|
||||
.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"]) {
|
||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||
await syncRoleResources(
|
||||
@@ -408,7 +437,9 @@ export async function updateProxyResources(
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null;
|
||||
const hcHeaders = healthcheckData?.headers
|
||||
? JSON.stringify(healthcheckData.headers)
|
||||
: null;
|
||||
|
||||
const [newHealthcheck] = await trx
|
||||
.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;
|
||||
|
||||
const [adminRole] = await trx
|
||||
|
||||
@@ -42,6 +42,10 @@ export const AuthSchema = z.object({
|
||||
// pincode has to have 6 digits
|
||||
pincode: z.number().min(100000).max(999999).optional(),
|
||||
password: z.string().min(1).optional(),
|
||||
"basic-auth": z.object({
|
||||
user: z.string().min(1),
|
||||
password: z.string().min(1)
|
||||
}).optional(),
|
||||
"sso-enabled": z.boolean().optional().default(false),
|
||||
"sso-roles": z
|
||||
.array(z.string())
|
||||
|
||||
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 { SupporterKey, supporterKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { license } from "@server/license/license";
|
||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
privateConfigSchema,
|
||||
readPrivateConfigFile
|
||||
} from "@server/lib/private/readConfigFile";
|
||||
import logger from "@server/logger";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export class Config {
|
||||
private rawConfig!: z.infer<typeof configSchema>;
|
||||
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
|
||||
|
||||
supporterData: SupporterKey | null = null;
|
||||
|
||||
@@ -37,19 +30,6 @@ export class Config {
|
||||
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 (
|
||||
// @ts-ignore
|
||||
parsedConfig.users ||
|
||||
@@ -109,132 +89,23 @@ export class Config {
|
||||
? "true"
|
||||
: "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) {
|
||||
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
|
||||
}
|
||||
|
||||
this.rawConfig = parsedConfig;
|
||||
this.rawPrivateConfig = parsedPrivateConfig;
|
||||
}
|
||||
|
||||
public async initServer() {
|
||||
if (!this.rawConfig) {
|
||||
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();
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
const licenseStatus = await license.check();
|
||||
if (
|
||||
!this.rawPrivateConfig.flags?.hide_supporter_key &&
|
||||
build != "oss" &&
|
||||
!licenseStatus.isHostLicensed
|
||||
) {
|
||||
if (build == "oss") {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
}
|
||||
@@ -243,10 +114,6 @@ export class Config {
|
||||
return this.rawConfig;
|
||||
}
|
||||
|
||||
public getRawPrivateConfig() {
|
||||
return this.rawPrivateConfig;
|
||||
}
|
||||
|
||||
public getNoReplyEmail(): string | undefined {
|
||||
return (
|
||||
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
|
||||
@@ -280,10 +147,6 @@ export class Config {
|
||||
return false;
|
||||
}
|
||||
|
||||
public isManagedMode() {
|
||||
return typeof this.rawConfig?.managed === "object";
|
||||
}
|
||||
|
||||
public async checkSupporterKey() {
|
||||
const [key] = await db.select().from(supporterKey).limit(1);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.10.4";
|
||||
export const APP_VERSION = "1.11.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
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 cors, { CorsOptions } from "cors";
|
||||
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 { getNextAvailableOrgSubnet } from "@server/lib/ip";
|
||||
import {
|
||||
@@ -28,9 +15,9 @@ import {
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { defaultRoleAllowedActions } from "@server/routers/role";
|
||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/private/billing";
|
||||
import { createCustomer } from "@server/routers/private/billing/createCustomer";
|
||||
import { usageService } from "@server/lib/private/billing/usageService";
|
||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
||||
import { createCustomer } from "#dynamic/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
|
||||
export async function createUserAccountOrg(
|
||||
userId: string,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { db, exitNodes, Transaction } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess(
|
||||
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
|
||||
const allExitNodes = await db
|
||||
.select({
|
||||
@@ -55,11 +59,24 @@ export function selectBestExitNode(
|
||||
return pingResults[0];
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
export async function checkExitNodeOrg(
|
||||
exitNodeId: number,
|
||||
orgId: string,
|
||||
trx?: Transaction | typeof db
|
||||
): Promise<boolean> {
|
||||
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
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,33 +1,4 @@
|
||||
import { build } from "@server/build";
|
||||
|
||||
// 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 "./exitNodes";
|
||||
export * from "./exitNodeComms";
|
||||
export * from "./subnet";
|
||||
export * from "./getCurrentExitNodeId";
|
||||
@@ -1,8 +1,5 @@
|
||||
import logger from "@server/logger";
|
||||
import { maxmindLookup } from "@server/db/maxmind";
|
||||
import axios from "axios";
|
||||
import config from "./config";
|
||||
import { tokenManager } from "./tokenManager";
|
||||
|
||||
export async function getCountryCodeForIp(
|
||||
ip: string
|
||||
@@ -33,32 +30,4 @@ export async function getCountryCodeForIp(
|
||||
}
|
||||
|
||||
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
|
||||
.object({
|
||||
app: z.object({
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase())
|
||||
.optional(),
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false),
|
||||
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
|
||||
app: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
endpoint: z.string().optional().default("https://pangolin.fossorial.io"),
|
||||
redirect_endpoint: z.string().optional()
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase())
|
||||
.optional(),
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false),
|
||||
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
|
||||
.record(
|
||||
z.string(),
|
||||
@@ -61,94 +55,95 @@ export const configSchema = z
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
server: z.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema
|
||||
.optional()
|
||||
.default(3000)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_port: portSchema
|
||||
.optional()
|
||||
.default(3001)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
next_port: portSchema
|
||||
.optional()
|
||||
.default(3002)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_hostname: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pangolin")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("p_session_token"),
|
||||
resource_access_token_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("p_token"),
|
||||
resource_access_token_headers: z
|
||||
.object({
|
||||
id: z.string().optional().default("P-Access-Token-Id"),
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("resource_session_request_param"),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
resource_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
cors: z
|
||||
.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
methods: z.array(z.string()).optional(),
|
||||
allowed_headers: z.array(z.string()).optional(),
|
||||
credentials: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||
secret: z
|
||||
.string()
|
||||
.pipe(z.string().min(8))
|
||||
.optional(),
|
||||
maxmind_db_path: z.string().optional()
|
||||
}).optional().default({
|
||||
integration_port: 3003,
|
||||
external_port: 3000,
|
||||
internal_port: 3001,
|
||||
next_port: 3002,
|
||||
internal_hostname: "pangolin",
|
||||
session_cookie_name: "p_session_token",
|
||||
resource_access_token_param: "p_token",
|
||||
resource_access_token_headers: {
|
||||
id: "P-Access-Token-Id",
|
||||
token: "P-Access-Token"
|
||||
},
|
||||
resource_session_request_param: "resource_session_request_param",
|
||||
dashboard_session_length_hours: 720,
|
||||
resource_session_length_hours: 720,
|
||||
trust_proxy: 1
|
||||
}),
|
||||
server: z
|
||||
.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema
|
||||
.optional()
|
||||
.default(3000)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_port: portSchema
|
||||
.optional()
|
||||
.default(3001)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
next_port: portSchema
|
||||
.optional()
|
||||
.default(3002)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_hostname: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pangolin")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("p_session_token"),
|
||||
resource_access_token_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("p_token"),
|
||||
resource_access_token_headers: z
|
||||
.object({
|
||||
id: z.string().optional().default("P-Access-Token-Id"),
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("resource_session_request_param"),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
resource_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
cors: z
|
||||
.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
methods: z.array(z.string()).optional(),
|
||||
allowed_headers: z.array(z.string()).optional(),
|
||||
credentials: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||
maxmind_db_path: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
integration_port: 3003,
|
||||
external_port: 3000,
|
||||
internal_port: 3001,
|
||||
next_port: 3002,
|
||||
internal_hostname: "pangolin",
|
||||
session_cookie_name: "p_session_token",
|
||||
resource_access_token_param: "p_token",
|
||||
resource_access_token_headers: {
|
||||
id: "P-Access-Token-Id",
|
||||
token: "P-Access-Token"
|
||||
},
|
||||
resource_session_request_param:
|
||||
"resource_session_request_param",
|
||||
dashboard_session_length_hours: 720,
|
||||
resource_session_length_hours: 720,
|
||||
trust_proxy: 1
|
||||
}),
|
||||
postgres: z
|
||||
.object({
|
||||
connection_string: z.string().optional(),
|
||||
@@ -158,7 +153,32 @@ export const configSchema = z
|
||||
connection_string: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
pool: z
|
||||
.object({
|
||||
max_connections: z
|
||||
.number()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(20),
|
||||
max_replica_connections: z
|
||||
.number()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(10),
|
||||
idle_timeout_ms: z
|
||||
.number()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(30000),
|
||||
connection_timeout_ms: z
|
||||
.number()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(5000)
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
})
|
||||
.optional(),
|
||||
traefik: z
|
||||
@@ -179,7 +199,10 @@ export const configSchema = z
|
||||
.optional()
|
||||
.default("/var/dynamic/router_config.yml"),
|
||||
static_domains: z.array(z.string()).optional().default([]),
|
||||
site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]),
|
||||
site_types: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default(["newt", "wireguard", "local"]),
|
||||
allow_raw_resources: z.boolean().optional().default(true),
|
||||
file_mode: z.boolean().optional().default(false)
|
||||
})
|
||||
@@ -291,14 +314,11 @@ export const configSchema = z
|
||||
nameservers: z
|
||||
.array(z.string().optional().optional())
|
||||
.optional()
|
||||
.default(["ns1.fossorial.io", "ns2.fossorial.io"]),
|
||||
cname_extension: z.string().optional().default("fossorial.io")
|
||||
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]),
|
||||
cname_extension: z.string().optional().default("cname.pangolin.net")
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
nameservers: ["ns1.fossorial.io", "ns2.fossorial.io"],
|
||||
cname_extension: "fossorial.io"
|
||||
})
|
||||
.default({})
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -306,10 +326,7 @@ export const configSchema = z
|
||||
if (data.flags?.disable_config_managed_domains) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is defined, domains are not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -321,15 +338,14 @@ export const configSchema = z
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If hybrid is defined, server secret is not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
|
||||
if (data.server?.secret === undefined) {
|
||||
data.server.secret = process.env.SERVER_SECRET;
|
||||
}
|
||||
return data.server?.secret !== undefined && data.server.secret.length > 0;
|
||||
return (
|
||||
data.server?.secret !== undefined &&
|
||||
data.server.secret.length > 0
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Server secret must be defined"
|
||||
@@ -337,12 +353,11 @@ export const configSchema = z
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If hybrid is defined, dashboard_url is not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is not defined, dashboard_url must be defined
|
||||
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
||||
return (
|
||||
data.app.dashboard_url !== undefined &&
|
||||
data.app.dashboard_url.length > 0
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Dashboard URL must be defined"
|
||||
@@ -374,7 +389,7 @@ export function readConfigFile() {
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
"No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file"
|
||||
"No configuration file found. Please create one. https://docs.pangolin.net/self-host/advanced/config-file"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { UserType } from "@server/types/UserTypes";
|
||||
import { build } from "@server/build";
|
||||
import license from "@server/license/license";
|
||||
|
||||
class TelemetryClient {
|
||||
private client: PostHog | null = null;
|
||||
@@ -32,7 +33,7 @@ class TelemetryClient {
|
||||
this.client = new PostHog(
|
||||
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
|
||||
{
|
||||
host: "https://digpangolin.com/relay-O7yI"
|
||||
host: "https://pangolin.net/relay-O7yI"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -47,11 +48,11 @@ class TelemetryClient {
|
||||
this.startAnalyticsInterval();
|
||||
|
||||
logger.info(
|
||||
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry"
|
||||
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry"
|
||||
);
|
||||
} else if (!this.enabled) {
|
||||
logger.info(
|
||||
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry"
|
||||
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.pangolin.net/telemetry"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -176,17 +177,36 @@ class TelemetryClient {
|
||||
|
||||
const stats = await this.getSystemStats();
|
||||
|
||||
this.client.capture({
|
||||
distinctId: hostMeta.hostMetaId,
|
||||
event: "supporter_status",
|
||||
properties: {
|
||||
valid: stats.supporterStatus.valid,
|
||||
tier: stats.supporterStatus.tier,
|
||||
github_username: stats.supporterStatus.githubUsername
|
||||
? this.anon(stats.supporterStatus.githubUsername)
|
||||
: "None"
|
||||
}
|
||||
});
|
||||
if (build === "enterprise") {
|
||||
const licenseStatus = await license.check();
|
||||
const payload = {
|
||||
distinctId: hostMeta.hostMetaId,
|
||||
event: "enterprise_status",
|
||||
properties: {
|
||||
is_host_licensed: licenseStatus.isHostLicensed,
|
||||
is_license_valid: licenseStatus.isLicenseValid,
|
||||
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({
|
||||
distinctId: hostMeta.hostMetaId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user