mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 14:20:44 +00:00
Compare commits
7 Commits
1.11.0-s.5
...
policies
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa44981cf | ||
|
|
1bacad7854 | ||
|
|
75cec731e8 | ||
|
|
16a88281bb | ||
|
|
1574cbc5df | ||
|
|
2cb2a115b0 | ||
|
|
9dce7b2cde |
@@ -29,6 +29,4 @@ CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
migrations/
|
||||
config/
|
||||
build.ts
|
||||
tsconfig.json
|
||||
config/
|
||||
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: amd64-runner
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,6 +47,4 @@ server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
*.mmdb
|
||||
24
Dockerfile
24
Dockerfile
@@ -15,29 +15,9 @@ RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||
|
||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
||||
|
||||
# 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 if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
||||
|
||||
RUN npm run build:$DATABASE
|
||||
RUN npm run build:cli
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
20
Makefile
20
Makefile
@@ -8,7 +8,6 @@ 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 \
|
||||
@@ -17,7 +16,6 @@ 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 \
|
||||
@@ -25,24 +23,6 @@ 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 .
|
||||
|
||||
145
README.md
145
README.md
@@ -1,36 +1,46 @@
|
||||
<div align="center">
|
||||
<h2>
|
||||
<a href="https://digpangolin.com">
|
||||
<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>
|
||||
<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">
|
||||
</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">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.digpangolin.com/">
|
||||
Documentation
|
||||
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
||||
Quick Install Guide
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="mailto:contact@fossorial.io">
|
||||
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>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://digpangolin.com/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://www.youtube.com/@fossorial-app)
|
||||
|
||||
</div>
|
||||
@@ -41,48 +51,107 @@
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Installation
|
||||
<img src="public/screenshots/hero.png" alt="Preview"/>
|
||||
|
||||
Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick-install) for how to install and set up Pangolin.
|
||||
|
||||
## Deployment Options
|
||||
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. |
|
||||

|
||||
|
||||
## Key Features
|
||||
|
||||
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||
### Reverse Proxy Through WireGuard Tunnel
|
||||
|
||||
| <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> |
|
||||
- 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.
|
||||
|
||||
## Get Started
|
||||
### Identity & Access Management
|
||||
|
||||
### Check out the docs
|
||||
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
|
||||
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
|
||||
- TOTP with backup codes for two-factor authentication.
|
||||
- Create organizations, each with multiple sites, users, and roles.
|
||||
- **Role-based access control** to manage resource access permissions.
|
||||
- Additional authentication options include:
|
||||
- Email whitelisting with **one-time passcodes.**
|
||||
- **Temporary, self-destructing share links.**
|
||||
- Resource specific pin codes.
|
||||
- Resource specific passwords.
|
||||
- Passkeys
|
||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||
- Auto-provision users and roles from your IdP.
|
||||
|
||||
We encourage everyone to read the full documentation first, which is
|
||||
available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
||||
|
||||
### Sign up and try now
|
||||
## Use Cases
|
||||
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started.
|
||||
### Manage Access to Internal Apps
|
||||
|
||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
||||
|
||||
### Developers and DevOps
|
||||
|
||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
||||
|
||||
### Secure API Gateway
|
||||
|
||||
- One application load balancer across multiple clouds and on-premises.
|
||||
|
||||
### IoT and Edge Devices
|
||||
|
||||
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
||||
|
||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Fully Self Hosted
|
||||
|
||||
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.
|
||||
|
||||
> 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 Cloud
|
||||
|
||||
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||
|
||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||
|
||||
### Managed & High Availability
|
||||
|
||||
Managed control plane, your infrastructure
|
||||
|
||||
- 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).
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io).
|
||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
||||
|
||||
## Contributions
|
||||
|
||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
||||
|
||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||
|
||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
||||
|
||||
If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`.
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
import { build } from "@server/build";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "pg", "schema"),
|
||||
];
|
||||
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")
|
||||
];
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { build } from "@server/build";
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "sqlite", "schema"),
|
||||
];
|
||||
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")
|
||||
];
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
|
||||
210
esbuild.mjs
210
esbuild.mjs
@@ -2,9 +2,8 @@ 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
|
||||
@@ -19,7 +18,7 @@ const require = topLevelCreateRequire(import.meta.url);
|
||||
`;
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage("Usage: $0 -entry [string] -out [string] -build [string]")
|
||||
.usage("Usage: $0 -entry [string] -out [string]")
|
||||
.option("entry", {
|
||||
alias: "e",
|
||||
describe: "Entry point file",
|
||||
@@ -32,13 +31,6 @@ 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;
|
||||
|
||||
@@ -54,179 +46,6 @@ 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],
|
||||
@@ -240,9 +59,6 @@ esbuild
|
||||
platform: "node",
|
||||
external: ["body-parser"],
|
||||
plugins: [
|
||||
privateImportGuardPlugin(),
|
||||
dynamicImportGuardPlugin(),
|
||||
dynamicImportSwitcherPlugin(argv.build),
|
||||
nodeExternalsPlugin({
|
||||
packagePath: getPackagePaths(),
|
||||
}),
|
||||
@@ -250,27 +66,7 @@ esbuild
|
||||
sourcemap: "inline",
|
||||
target: "node22",
|
||||
})
|
||||
.then((result) => {
|
||||
// Check if there were any errors in the build result
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||
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);
|
||||
}
|
||||
|
||||
.then(() => {
|
||||
console.log("Build completed successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
|
||||
{{if .HybridMode}}
|
||||
managed:
|
||||
id: "{{.HybridId}}"
|
||||
secret: "{{.HybridSecret}}"
|
||||
|
||||
{{else}}
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
@@ -23,7 +28,6 @@ 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}}"
|
||||
@@ -36,4 +40,5 @@ flags:
|
||||
require_email_verification: {{.EnableEmail}}
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
allow_raw_resources: true
|
||||
{{end}}
|
||||
@@ -6,6 +6,8 @@ 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"
|
||||
@@ -20,7 +22,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
@@ -31,7 +33,7 @@ services:
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
- 21820:21820/udp
|
||||
- 443:443
|
||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
@@ -54,9 +56,15 @@ 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}}
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
|
||||
volumes:
|
||||
pangolin-data:
|
||||
|
||||
@@ -3,12 +3,17 @@ 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:
|
||||
@@ -22,7 +27,7 @@ log:
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
{{if not .HybridMode}}
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
@@ -31,18 +36,22 @@ 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"
|
||||
http:
|
||||
{{if not .HybridMode}} http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
certResolver: "letsencrypt"{{end}}
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get installer - Cross-platform installation script
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# GitHub repository info
|
||||
REPO="fosrl/pangolin"
|
||||
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to get latest version from GitHub API
|
||||
get_latest_version() {
|
||||
local latest_info
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$latest_info" ]; then
|
||||
print_error "Failed to fetch latest version information" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version from JSON response (works without jq)
|
||||
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
print_error "Could not parse version from GitHub API response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove 'v' prefix if present
|
||||
version=$(echo "$version" | sed 's/^v//')
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
local os arch
|
||||
|
||||
# Detect OS - only support Linux
|
||||
case "$(uname -s)" in
|
||||
Linux*) os="linux" ;;
|
||||
*)
|
||||
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect architecture - only support amd64 and arm64
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
arm64|aarch64) arch="arm64" ;;
|
||||
*)
|
||||
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "${os}_${arch}"
|
||||
}
|
||||
|
||||
# Get installation directory
|
||||
get_install_dir() {
|
||||
# Install to the current directory
|
||||
local install_dir="$(pwd)"
|
||||
if [ ! -d "$install_dir" ]; then
|
||||
print_error "Installation directory does not exist: $install_dir"
|
||||
exit 1
|
||||
fi
|
||||
echo "$install_dir"
|
||||
}
|
||||
|
||||
# Download and install installer
|
||||
install_installer() {
|
||||
local platform="$1"
|
||||
local install_dir="$2"
|
||||
local binary_name="installer_${platform}"
|
||||
|
||||
local download_url="${BASE_URL}/${binary_name}"
|
||||
local temp_file="/tmp/installer"
|
||||
local final_path="${install_dir}/installer"
|
||||
|
||||
print_status "Downloading installer from ${download_url}"
|
||||
|
||||
# Download the binary
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$download_url" -o "$temp_file"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q "$download_url" -O "$temp_file"
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install directory if it doesn't exist
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
# Move binary to install directory
|
||||
mv "$temp_file" "$final_path"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$final_path"
|
||||
|
||||
print_status "Installer downloaded to ${final_path}"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_installation() {
|
||||
local install_dir="$1"
|
||||
local installer_path="${install_dir}/installer"
|
||||
|
||||
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
|
||||
print_status "Installation successful!"
|
||||
return 0
|
||||
else
|
||||
print_error "Installation failed. Binary not found or not executable."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation process
|
||||
main() {
|
||||
print_status "Installing latest version of installer..."
|
||||
|
||||
# Get latest version
|
||||
print_status "Fetching latest version from GitHub..."
|
||||
VERSION=$(get_latest_version)
|
||||
print_status "Latest version: v${VERSION}"
|
||||
|
||||
# Set base URL with the fetched version
|
||||
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||
|
||||
# Detect platform
|
||||
PLATFORM=$(detect_platform)
|
||||
print_status "Detected platform: ${PLATFORM}"
|
||||
|
||||
# Get install directory
|
||||
INSTALL_DIR=$(get_install_dir)
|
||||
print_status "Install directory: ${INSTALL_DIR}"
|
||||
|
||||
# Install installer
|
||||
install_installer "$PLATFORM" "$INSTALL_DIR"
|
||||
|
||||
# Verify installation
|
||||
if verify_installation "$INSTALL_DIR"; then
|
||||
print_status "Installer is ready to use!"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -3,8 +3,8 @@ module installer
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/term v0.35.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.37.0 // indirect
|
||||
require golang.org/x/sys v0.36.0 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/sys v0.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=
|
||||
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,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -47,8 +48,10 @@ type Config struct {
|
||||
InstallGerbil bool
|
||||
TraefikBouncerKey string
|
||||
DoCrowdsecInstall bool
|
||||
EnableGeoblocking bool
|
||||
Secret string
|
||||
HybridMode bool
|
||||
HybridId string
|
||||
HybridSecret string
|
||||
}
|
||||
|
||||
type SupportedContainer string
|
||||
@@ -95,6 +98,24 @@ 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)
|
||||
@@ -104,15 +125,6 @@ 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) {
|
||||
@@ -160,34 +172,9 @@ 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() {
|
||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Println("\n=== CrowdSec Install ===")
|
||||
// check if crowdsec is installed
|
||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||
@@ -243,7 +230,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyInstalled {
|
||||
if !config.HybridMode && !alreadyInstalled {
|
||||
// Setup Token Section
|
||||
fmt.Println("\n=== Setup Token ===")
|
||||
|
||||
@@ -264,7 +251,9 @@ func main() {
|
||||
|
||||
fmt.Println("\nInstallation complete!")
|
||||
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
}
|
||||
|
||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||
@@ -339,38 +328,66 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
|
||||
// Basic configuration
|
||||
fmt.Println("\n=== Basic Configuration ===")
|
||||
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.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", "")
|
||||
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'")
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced configuration
|
||||
@@ -378,7 +395,6 @@ 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")
|
||||
@@ -413,6 +429,11 @@ 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
|
||||
@@ -642,30 +663,18 @@ func checkPortsAvailable(port int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadMaxMindDatabase() error {
|
||||
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||
|
||||
// Download the GeoLite2 Country database
|
||||
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
|
||||
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
|
||||
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
|
||||
func checkIsPangolinInstalledWithHybrid() bool {
|
||||
// Check if config/config.yml exists and contains hybrid section
|
||||
if _, err := os.Stat("config/config.yml"); 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)
|
||||
|
||||
// Read config file to check for hybrid section
|
||||
content, err := os.ReadFile("config/config.yml")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Check for hybrid section
|
||||
return bytes.Contains(content, []byte("managed:"))
|
||||
}
|
||||
|
||||
110
install/quickStart.go
Normal file
110
install/quickStart.go
Normal file
@@ -0,0 +1,110 @@
|
||||
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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Само локални ресурси. Без тунелиране. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
|
||||
"siteSeeAll": "Вижте всички сайтове",
|
||||
"siteTunnelDescription": "Определете как искате да се свържете с вашия сайт",
|
||||
"siteNewtCredentials": "Newt Удостоверения",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Създаден на",
|
||||
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
|
||||
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
|
||||
"proxyEnableSSL": "Активиране на SSL",
|
||||
"proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.",
|
||||
"target": "Цел",
|
||||
"configureTarget": "Конфигуриране на цели",
|
||||
"proxyEnableSSL": "Активиране на SSL (https)",
|
||||
"targetErrorFetch": "Неуспешно извличане на цели",
|
||||
"targetErrorFetchDescription": "Възникна грешка при извличане на целите",
|
||||
"siteErrorFetch": "Неуспешно извличане на ресурс",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Конфигурация на защитена връзка",
|
||||
"targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс",
|
||||
"targetTlsSettingsAdvanced": "Разширени TLS настройки",
|
||||
"targetTlsSni": "Имя на TLS сървър",
|
||||
"targetTlsSni": "Име на TLS сървър (SNI)",
|
||||
"targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.",
|
||||
"targetTlsSubmit": "Запазване на настройките",
|
||||
"targets": "Конфигурация на целите",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.",
|
||||
"methodSelect": "Изберете метод",
|
||||
"targetSubmit": "Добавяне на цел",
|
||||
"targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.",
|
||||
"targetNoOne": "Няма цели. Добавете цел чрез формата.",
|
||||
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
||||
"targetsSubmit": "Запазване на целите",
|
||||
"addTarget": "Добавете цел",
|
||||
"targetErrorInvalidIp": "Невалиден IP адрес",
|
||||
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
||||
"targetErrorInvalidPort": "Невалиден порт",
|
||||
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
|
||||
"targetErrorNoSite": "Няма избран сайт",
|
||||
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
|
||||
"targetCreated": "Целта е създадена",
|
||||
"targetCreatedDescription": "Целта беше успешно създадена",
|
||||
"targetErrorCreate": "Неуспешно създаване на целта",
|
||||
"targetErrorCreateDescription": "Възникна грешка при създаването на целта",
|
||||
"save": "Запази",
|
||||
"proxyAdditional": "Допълнителни настройки на прокси",
|
||||
"proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки",
|
||||
"proxyCustomHeader": "Персонализиран хост заглавие",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Администратор на сървър - Панголин",
|
||||
"licenseTierProfessional": "Професионален лиценз",
|
||||
"licenseTierEnterprise": "Предприятие лиценз",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Търговски лиценз",
|
||||
"licensed": "Лицензиран",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
@@ -765,7 +750,7 @@
|
||||
"idpDisplayName": "Име за показване за този доставчик на идентичност",
|
||||
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
|
||||
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
|
||||
"licenseBadge": "EE",
|
||||
"licenseBadge": "Професионален",
|
||||
"idpType": "Тип доставчик",
|
||||
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
|
||||
"idpOidcConfigure": "Конфигурация на OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"navbar": "Навигационно меню",
|
||||
"navbarDescription": "Главно навигационно меню за приложението",
|
||||
"navbarDocsLink": "Документация",
|
||||
"commercialEdition": "Търговско издание",
|
||||
"otpErrorEnable": "Не може да се активира 2FA",
|
||||
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
|
||||
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Всички потребители",
|
||||
"sidebarIdentityProviders": "Идентификационни доставчици",
|
||||
"sidebarLicense": "Лиценз",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Клиенти (Бета)",
|
||||
"sidebarDomains": "Домейни",
|
||||
"enableDockerSocket": "Активиране на Docker Чернова",
|
||||
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.",
|
||||
"twoFactor": "Двуфакторно удостоверяване",
|
||||
"adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.",
|
||||
"continueToApplication": "Продължете към приложението",
|
||||
"securityKeyAdd": "Добавяне на ключ за сигурност",
|
||||
"securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност",
|
||||
"securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "Външен прокси разрешен",
|
||||
"addNewTarget": "Добави нова цел",
|
||||
"targetsList": "Списък с цели",
|
||||
"advancedMode": "Разширен режим",
|
||||
"targetErrorDuplicateTargetFound": "Дублирана цел намерена",
|
||||
"healthCheckHealthy": "Здрав",
|
||||
"healthCheckUnhealthy": "Нездрав",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Грешка при автоматично влизане",
|
||||
"autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.",
|
||||
"autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Отдалечени възли",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Управление на самостоятелно хоствани",
|
||||
"remoteExitNodeDescription": "Управление на възли за разширяване на мрежовата ви свързаност",
|
||||
"remoteExitNodes": "Възли",
|
||||
"searchRemoteExitNodes": "Търсене на възли...",
|
||||
"remoteExitNodeAdd": "Добавяне на възел",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.",
|
||||
"remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)",
|
||||
"remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)",
|
||||
"sidebarRemoteExitNodes": "Отдалечени възли",
|
||||
"sidebarRemoteExitNodes": "Възли (\"Local\" match)",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Създаване на възел",
|
||||
"description": "Създайте нов възел, за да разширите мрежовата си свързаност",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Страницата за удостоверяване е актуализирана успешно",
|
||||
"healthCheckNotAvailable": "Локална",
|
||||
"rewritePath": "Пренапиши път",
|
||||
"rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта.",
|
||||
"continueToApplication": "Продължете до приложението",
|
||||
"checkingInvite": "Проверка на поканата",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Премахване на автентикация в заглавката",
|
||||
"resourceHeaderAuthRemoveDescription": "Автентикацията в заглавката беше премахната успешно.",
|
||||
"resourceErrorHeaderAuthRemove": "Неуспешно премахване на автентикация в заглавката",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Не беше възможно премахването на автентикацията в заглавката за ресурса.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Неуспешно задаване на автентикация в заглавката",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Не беше възможно задаването на автентикация в заглавката за ресурса.",
|
||||
"resourceHeaderAuthSetup": "Автентикацията в заглавката беше зададена успешно",
|
||||
"resourceHeaderAuthSetupDescription": "Автентикацията в заглавката беше успешно зададена.",
|
||||
"resourceHeaderAuthSetupTitle": "Задаване на автентикация в заглавката",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Задаване на автентикация в заглавката",
|
||||
"actionSetResourceHeaderAuth": "Задаване на автентикация в заглавката",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Приоритет",
|
||||
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||
"siteSeeAll": "Zobrazit všechny lokality",
|
||||
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
||||
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Vytvořeno v",
|
||||
"proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.",
|
||||
"proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.",
|
||||
"proxyEnableSSL": "Povolit SSL",
|
||||
"proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená HTTPS připojení k vašim cílům.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Konfigurace cílů",
|
||||
"proxyEnableSSL": "Povolit SSL (https)",
|
||||
"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",
|
||||
@@ -498,7 +495,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",
|
||||
"targetTlsSni": "Název serveru TLS (SNI)",
|
||||
"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ů",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
|
||||
"methodSelect": "Vyberte metodu",
|
||||
"targetSubmit": "Add Target",
|
||||
"targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na vaši backend.",
|
||||
"targetNoOne": "Žádné cíle. Přidejte cíl pomocí formuláře.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Správce serveru - Pangolin",
|
||||
"licenseTierProfessional": "Profesionální licence",
|
||||
"licenseTierEnterprise": "Podniková licence",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Obchodní licence",
|
||||
"licensed": "Licencováno",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profesionální",
|
||||
"idpType": "Typ poskytovatele",
|
||||
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
|
||||
"idpOidcConfigure": "Nastavení OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Všichni uživatelé",
|
||||
"sidebarIdentityProviders": "Poskytovatelé identity",
|
||||
"sidebarLicense": "Licence",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Klienti (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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é",
|
||||
@@ -1557,8 +1543,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": "Vzdálené uzly",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Spravovat vlastní hostování",
|
||||
"remoteExitNodeDescription": "Spravujte uzly pro rozšíření připojení k síti",
|
||||
"remoteExitNodes": "Uzly",
|
||||
"searchRemoteExitNodes": "Hledat uzly...",
|
||||
"remoteExitNodeAdd": "Přidat uzel",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.",
|
||||
"remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu",
|
||||
"remoteExitNodeDelete": "Odstranit uzel",
|
||||
"sidebarRemoteExitNodes": "Vzdálené uzly",
|
||||
"sidebarRemoteExitNodes": "Uzly",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Vytvořit uzel",
|
||||
"description": "Vytvořit nový uzel pro rozšíření síťového připojení",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Autentizační stránka byla úspěšně aktualizována",
|
||||
"healthCheckNotAvailable": "Místní",
|
||||
"rewritePath": "Přepsat cestu",
|
||||
"rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl.",
|
||||
"continueToApplication": "Pokračovat v aplikaci",
|
||||
"checkingInvite": "Kontrola pozvánky",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Odstranit Autentizaci Záhlaví",
|
||||
"resourceHeaderAuthRemoveDescription": "Úspěšně odstraněna autentizace záhlaví.",
|
||||
"resourceErrorHeaderAuthRemove": "Nepodařilo se odstranit Autentizaci Záhlaví",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Nepodařilo se odstranit autentizaci záhlaví ze zdroje.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Nepodařilo se nastavit Autentizaci Záhlaví",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Nepodařilo se nastavit autentizaci záhlaví ze zdroje.",
|
||||
"resourceHeaderAuthSetup": "Úspěšně nastavena Autentizace Záhlaví",
|
||||
"resourceHeaderAuthSetupDescription": "Autentizace záhlaví byla úspěšně nastavena.",
|
||||
"resourceHeaderAuthSetupTitle": "Nastavit Autentizaci Záhlaví",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Nastavit Autentizaci Záhlaví",
|
||||
"actionSetResourceHeaderAuth": "Nastavit Autentizaci Záhlaví",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priorita",
|
||||
"priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||
"siteSeeAll": "Alle Standorte anzeigen",
|
||||
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
||||
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Erstellt am",
|
||||
"proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.",
|
||||
"proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.",
|
||||
"proxyEnableSSL": "SSL aktivieren",
|
||||
"proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Ziele konfigurieren",
|
||||
"proxyEnableSSL": "SSL aktivieren (https)",
|
||||
"targetErrorFetch": "Fehler beim Abrufen der Ziele",
|
||||
"targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten",
|
||||
"siteErrorFetch": "Fehler beim Abrufen der Ressource",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Sicherheitskonfiguration",
|
||||
"targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource",
|
||||
"targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen",
|
||||
"targetTlsSni": "TLS Servername",
|
||||
"targetTlsSni": "TLS-Servername (SNI)",
|
||||
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
||||
"targetTlsSubmit": "Einstellungen speichern",
|
||||
"targets": "Ziel-Konfiguration",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
||||
"methodSelect": "Methode auswählen",
|
||||
"targetSubmit": "Ziel hinzufügen",
|
||||
"targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.",
|
||||
"targetNoOne": "Keine Ziele. Fügen Sie ein Ziel über das Formular hinzu.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||
"licenseTierProfessional": "Professional Lizenz",
|
||||
"licenseTierEnterprise": "Enterprise Lizenz",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Gewerbliche Lizenz",
|
||||
"licensed": "Lizenziert",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profi",
|
||||
"idpType": "Anbietertyp",
|
||||
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Konfiguration",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Alle Benutzer",
|
||||
"sidebarIdentityProviders": "Identitätsanbieter",
|
||||
"sidebarLicense": "Lizenz",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Kunden (Beta)",
|
||||
"sidebarDomains": "Domänen",
|
||||
"enableDockerSocket": "Docker Blaupause aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "Externer Proxy aktiviert",
|
||||
"addNewTarget": "Neues Ziel hinzufügen",
|
||||
"targetsList": "Ziel-Liste",
|
||||
"advancedMode": "Erweiterter Modus",
|
||||
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
|
||||
"healthCheckHealthy": "Gesund",
|
||||
"healthCheckUnhealthy": "Ungesund",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
||||
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
||||
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Selbst-Hosted verwalten",
|
||||
"remoteExitNodeDescription": "Knoten verwalten, um die Netzwerkverbindung zu erweitern",
|
||||
"remoteExitNodes": "Knoten",
|
||||
"searchRemoteExitNodes": "Knoten suchen...",
|
||||
"remoteExitNodeAdd": "Knoten hinzufügen",
|
||||
@@ -1568,7 +1554,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": "Entfernte Knoten",
|
||||
"sidebarRemoteExitNodes": "Knoten",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Knoten erstellen",
|
||||
"description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Auth-Seite erfolgreich aktualisiert",
|
||||
"healthCheckNotAvailable": "Lokal",
|
||||
"rewritePath": "Pfad neu schreiben",
|
||||
"rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird.",
|
||||
"continueToApplication": "Weiter zur Anwendung",
|
||||
"checkingInvite": "Einladung wird überprüft",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Header-Auth entfernen",
|
||||
"resourceHeaderAuthRemoveDescription": "Header-Authentifizierung erfolgreich entfernt.",
|
||||
"resourceErrorHeaderAuthRemove": "Fehler beim Entfernen der Header-Authentifizierung",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Die Headerauthentifizierung für die Ressource konnte nicht entfernt werden.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Fehler beim Setzen der Header-Authentifizierung",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Konnte Header-Authentifizierung für die Ressource nicht festlegen.",
|
||||
"resourceHeaderAuthSetup": "Header-Authentifizierung erfolgreich festgelegt",
|
||||
"resourceHeaderAuthSetupDescription": "Header-Authentifizierung wurde erfolgreich festgelegt.",
|
||||
"resourceHeaderAuthSetupTitle": "Header-Authentifizierung festlegen",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Header-Authentifizierung festlegen",
|
||||
"actionSetResourceHeaderAuth": "Header-Authentifizierung festlegen",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priorität",
|
||||
"priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
{
|
||||
"setupCreate": "Create your organization, site, and resources",
|
||||
"setupNewOrg": "New Organization",
|
||||
@@ -96,7 +97,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. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling.",
|
||||
"siteSeeAll": "See All Sites",
|
||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||
"siteNewtCredentials": "Newt Credentials",
|
||||
@@ -468,10 +469,7 @@
|
||||
"createdAt": "Created At",
|
||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
||||
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||
"proxyEnableSSL": "Enable SSL",
|
||||
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configure Targets",
|
||||
"proxyEnableSSL": "Enable SSL (https)",
|
||||
"targetErrorFetch": "Failed to fetch targets",
|
||||
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
||||
"siteErrorFetch": "Failed to fetch resource",
|
||||
@@ -498,7 +496,7 @@
|
||||
"targetTlsSettings": "Secure Connection Configuration",
|
||||
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
||||
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
||||
"targetTlsSni": "TLS Server Name",
|
||||
"targetTlsSni": "TLS Server Name (SNI)",
|
||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||
"targetTlsSubmit": "Save Settings",
|
||||
"targets": "Targets Configuration",
|
||||
@@ -507,21 +505,9 @@
|
||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||
"methodSelect": "Select method",
|
||||
"targetSubmit": "Add Target",
|
||||
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
|
||||
"targetNoOne": "No targets. Add a target using the form.",
|
||||
"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",
|
||||
@@ -730,7 +716,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
"licenseTierProfessional": "Professional License",
|
||||
"licenseTierEnterprise": "Enterprise License",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Commercial License",
|
||||
"licensed": "Licensed",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
@@ -765,7 +751,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": "EE",
|
||||
"licenseBadge": "Professional",
|
||||
"idpType": "Provider Type",
|
||||
"idpTypeDescription": "Select the type of identity provider you want to configure",
|
||||
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
||||
@@ -1055,6 +1041,26 @@
|
||||
"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",
|
||||
@@ -1099,6 +1105,7 @@
|
||||
"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",
|
||||
@@ -1149,12 +1156,13 @@
|
||||
"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",
|
||||
"sidebarClients": "Clients (Beta)",
|
||||
"sidebarDomains": "Domains",
|
||||
"enableDockerSocket": "Enable Docker Blueprint",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||
@@ -1347,6 +1355,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1433,6 @@
|
||||
"externalProxyEnabled": "External Proxy Enabled",
|
||||
"addNewTarget": "Add New Target",
|
||||
"targetsList": "Targets List",
|
||||
"advancedMode": "Advanced Mode",
|
||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||
"healthCheckHealthy": "Healthy",
|
||||
"healthCheckUnhealthy": "Unhealthy",
|
||||
@@ -1557,8 +1565,8 @@
|
||||
"autoLoginError": "Auto Login Error",
|
||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted",
|
||||
"remoteExitNodeDescription": "Manage nodes to extend your network connectivity",
|
||||
"remoteExitNodes": "Nodes",
|
||||
"searchRemoteExitNodes": "Search nodes...",
|
||||
"remoteExitNodeAdd": "Add Node",
|
||||
@@ -1568,7 +1576,7 @@
|
||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||
"remoteExitNodeDelete": "Delete Node",
|
||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||
"sidebarRemoteExitNodes": "Nodes",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Create Node",
|
||||
"description": "Create a new node to extend your network connectivity",
|
||||
@@ -1738,161 +1746,5 @@
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"rewritePath": "Rewrite Path",
|
||||
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
||||
"continueToApplication": "Continue to application",
|
||||
"checkingInvite": "Checking Invite",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Remove Header Auth",
|
||||
"resourceHeaderAuthRemoveDescription": "Header authentication removed successfully.",
|
||||
"resourceErrorHeaderAuthRemove": "Failed to remove Header Authentication",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Could not remove header authentication for the resource.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Failed to set Header Authentication",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Could not set header authentication for the resource.",
|
||||
"resourceHeaderAuthSetup": "Header Authentication set successfully",
|
||||
"resourceHeaderAuthSetupDescription": "Header authentication has been successfully set.",
|
||||
"resourceHeaderAuthSetupTitle": "Set Header Authentication",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Set Header Authentication",
|
||||
"actionSetResourceHeaderAuth": "Set Header Authentication",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that the information I provided is accurate and that I am in compliance with the Fossorial Commercial License. Reporting inaccurate information or misidentifying use of the product is a violation of the license and may result in your key getting revoked."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priority",
|
||||
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip",
|
||||
"sidebarEnableEnterpriseLicense": "Enable Enterprise License"
|
||||
"continueToApplication": "Continue to application"
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||
"siteSeeAll": "Ver todos los sitios",
|
||||
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
||||
"siteNewtCredentials": "Credenciales nuevas",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Creado el",
|
||||
"proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.",
|
||||
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
|
||||
"proxyEnableSSL": "Activar SSL",
|
||||
"proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurar objetivos",
|
||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
||||
"targetErrorFetch": "Error al recuperar los objetivos",
|
||||
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
|
||||
"siteErrorFetch": "No se pudo obtener el recurso",
|
||||
@@ -498,7 +495,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",
|
||||
"targetTlsSni": "Nombre del servidor TLS (SNI)",
|
||||
"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",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.",
|
||||
"methodSelect": "Seleccionar método",
|
||||
"targetSubmit": "Añadir destino",
|
||||
"targetNoOne": "Este recurso no tiene ningún objetivo. Agrega un objetivo para configurar dónde enviar peticiones al backend.",
|
||||
"targetNoOne": "No hay objetivos. Agregue un objetivo usando el formulario.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Admin Servidor - Pangolin",
|
||||
"licenseTierProfessional": "Licencia profesional",
|
||||
"licenseTierEnterprise": "Licencia Enterprise",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Licencia comercial",
|
||||
"licensed": "Licenciado",
|
||||
"yes": "Sí",
|
||||
"no": "Nu",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profesional",
|
||||
"idpType": "Tipo de proveedor",
|
||||
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
|
||||
"idpOidcConfigure": "Configuración OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Todos los usuarios",
|
||||
"sidebarIdentityProviders": "Proveedores de identidad",
|
||||
"sidebarLicense": "Licencia",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Clientes (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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",
|
||||
@@ -1557,8 +1543,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": "Nodos remotos",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Administrar Nodos Autogestionados",
|
||||
"remoteExitNodeDescription": "Administrar nodos para extender la conectividad de red",
|
||||
"remoteExitNodes": "Nodos",
|
||||
"searchRemoteExitNodes": "Buscar nodos...",
|
||||
"remoteExitNodeAdd": "Añadir Nodo",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.",
|
||||
"remoteExitNodeConfirmDelete": "Confirmar eliminar nodo",
|
||||
"remoteExitNodeDelete": "Eliminar Nodo",
|
||||
"sidebarRemoteExitNodes": "Nodos remotos",
|
||||
"sidebarRemoteExitNodes": "Nodos",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Crear Nodo",
|
||||
"description": "Crear un nuevo nodo para extender la conectividad de red",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Página auth actualizada correctamente",
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"rewritePath": "Reescribir Ruta",
|
||||
"rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino.",
|
||||
"continueToApplication": "Continuar a la aplicación",
|
||||
"checkingInvite": "Comprobando invitación",
|
||||
"setResourceHeaderAuth": "set-Resource HeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Eliminar Auth del Encabezado",
|
||||
"resourceHeaderAuthRemoveDescription": "Autenticación de cabecera eliminada correctamente.",
|
||||
"resourceErrorHeaderAuthRemove": "Error al eliminar autenticación de cabecera",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "No se pudo eliminar la autenticación de cabecera del recurso.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Error al establecer autenticación de cabecera",
|
||||
"resourceErrorHeaderAuthSetupDescription": "No se pudo establecer autenticación de cabecera para el recurso.",
|
||||
"resourceHeaderAuthSetup": "Autenticación de cabecera establecida correctamente",
|
||||
"resourceHeaderAuthSetupDescription": "La autenticación de cabecera se ha establecido correctamente.",
|
||||
"resourceHeaderAuthSetupTitle": "Establecer autenticación de cabecera",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Establecer autenticación de cabecera",
|
||||
"actionSetResourceHeaderAuth": "Establecer autenticación de cabecera",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Prioridad",
|
||||
"priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino."
|
||||
}
|
||||
|
||||
@@ -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 de votre organisation. Il est différent du nom affiché.",
|
||||
"setupErrorIdentifier": "Cet identifiant est déjà pris. Veuillez en choisir un autre.",
|
||||
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.",
|
||||
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
||||
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
||||
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
||||
"welcome": "Bienvenue sur Pangolin",
|
||||
"welcome": "Bienvenue à Pangolin",
|
||||
"welcomeTo": "Bienvenue chez",
|
||||
"componentsCreateOrg": "Créer une organisation",
|
||||
"componentsMember": "Vous {count, plural, =0 {n'} other {}}êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
|
||||
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
||||
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"dismiss": "Refuser",
|
||||
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation 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",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
|
||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.",
|
||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
|
||||
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
||||
"goHome": "Retour à la maison",
|
||||
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
|
||||
"createAnAccount": "Créer un compte",
|
||||
"inviteNotAccepted": "Invitation non acceptée",
|
||||
"authCreateAccount": "Créez un compte pour commencer",
|
||||
"authNoAccount": "Vous n'avez pas de compte ?",
|
||||
"email": "Adresse email",
|
||||
"email": "Courriel",
|
||||
"password": "Mot de passe",
|
||||
"confirmPassword": "Confirmer le mot de passe",
|
||||
"createAccount": "Créer un compte",
|
||||
"viewSettings": "Afficher les paramètres",
|
||||
"delete": "Supprimer",
|
||||
"delete": "Supprimez",
|
||||
"name": "Nom",
|
||||
"online": "En ligne",
|
||||
"offline": "Hors ligne",
|
||||
"site": "Site",
|
||||
"dataIn": "Données reçues",
|
||||
"dataOut": "Données émises",
|
||||
"dataIn": "Données dans",
|
||||
"dataOut": "Données épuisées",
|
||||
"connectionType": "Type de connexion",
|
||||
"tunnelType": "Type de tunnel",
|
||||
"local": "Locale",
|
||||
"edit": "Modifier",
|
||||
"edit": "Editer",
|
||||
"siteConfirmDelete": "Confirmer la suppression du site",
|
||||
"siteDelete": "Supprimer le site",
|
||||
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.",
|
||||
@@ -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": "Afficher les détails du déploiement Docker",
|
||||
"siteDocker": "Développer les détails du déploiement Docker",
|
||||
"toggle": "Activer/désactiver",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"dockerRun": "Docker Run",
|
||||
"siteLearnLocal": "Les sites locaux ne permettent pas d'utiliser les tunnel, en savoir plus",
|
||||
"dockerCompose": "Composition Docker",
|
||||
"dockerRun": "Exécution Docker",
|
||||
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus",
|
||||
"siteConfirmCopy": "J'ai copié la configuration",
|
||||
"searchSitesProgress": "Rechercher des sites...",
|
||||
"siteAdd": "Ajouter un site",
|
||||
@@ -79,9 +79,9 @@
|
||||
"operatingSystem": "Système d'exploitation",
|
||||
"commands": "Commandes",
|
||||
"recommended": "Recommandé",
|
||||
"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",
|
||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
||||
"siteRunsInDocker": "Exécute dans Docker",
|
||||
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows",
|
||||
"siteErrorDelete": "Erreur lors de la suppression du site",
|
||||
"siteErrorUpdate": "Impossible de mettre à jour le site",
|
||||
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
|
||||
@@ -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 de {siteName}",
|
||||
"siteSetting": "Réglages {siteName}",
|
||||
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
|
||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
|
||||
"siteWg": "WireGuard basique",
|
||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||
"siteLocalDescriptionSaas": "Ressources locales seulement. Pas de tunneling. Seulement disponible sur les noeuds distants",
|
||||
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteSeeAll": "Voir tous les sites",
|
||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||
"siteNewtCredentials": "Identifiants Newt",
|
||||
"siteNewtCredentialsDescription": "C'est comme cela que Newt s'authentifiera avec le serveur",
|
||||
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur",
|
||||
"siteCredentialsSave": "Enregistrez vos identifiants",
|
||||
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
|
||||
"siteInfo": "Informations sur le site",
|
||||
@@ -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 de partage a été supprimé",
|
||||
"shareDeletedDescription": "Le lien a été supprimé",
|
||||
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
||||
"accessToken": "Jeton d'accès",
|
||||
"usageExamples": "Exemples d'utilisation",
|
||||
@@ -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 l'utilisation du jeton d'accès",
|
||||
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
|
||||
"createLink": "Créer un lien",
|
||||
"resourcesNotFound": "Aucune ressource trouvée",
|
||||
"resourceSearch": "Rechercher des ressources",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Créé le",
|
||||
"proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.",
|
||||
"proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.",
|
||||
"proxyEnableSSL": "Activer SSL",
|
||||
"proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurer les cibles",
|
||||
"proxyEnableSSL": "Activer SSL (https)",
|
||||
"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",
|
||||
@@ -498,7 +495,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 du serveur TLS",
|
||||
"targetTlsSni": "Nom de serveur TLS (SNI)",
|
||||
"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",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
||||
"methodSelect": "Sélectionner la méthode",
|
||||
"targetSubmit": "Ajouter une cible",
|
||||
"targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.",
|
||||
"targetNoOne": "Aucune cible. Ajoutez une cible en utilisant le formulaire.",
|
||||
"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é",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Admin Serveur - Pangolin",
|
||||
"licenseTierProfessional": "Licence Professionnelle",
|
||||
"licenseTierEnterprise": "Licence Entreprise",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Licence commerciale",
|
||||
"licensed": "Sous licence",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Professionnel",
|
||||
"idpType": "Type de fournisseur",
|
||||
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
|
||||
"idpOidcConfigure": "Configuration OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Tous les utilisateurs",
|
||||
"sidebarIdentityProviders": "Fournisseurs d'identité",
|
||||
"sidebarLicense": "Licence",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Clients (Bêta)",
|
||||
"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.",
|
||||
@@ -1234,7 +1220,7 @@
|
||||
"billing": "Facturation",
|
||||
"orgBillingDescription": "Gérez vos informations de facturation et vos abonnements",
|
||||
"github": "GitHub",
|
||||
"pangolinHosted": "Hebergé par Pangolin",
|
||||
"pangolinHosted": "Pangolin Hébergement",
|
||||
"fossorial": "Fossorial",
|
||||
"completeAccountSetup": "Complétez la configuration du compte",
|
||||
"completeAccountSetupDescription": "Définissez votre mot de passe pour commencer",
|
||||
@@ -1316,7 +1302,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": "Erreur",
|
||||
"failed": "Échec",
|
||||
"createNewOrgDescription": "Créer une nouvelle organisation",
|
||||
"organization": "Organisation",
|
||||
"port": "Port",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1370,7 +1357,7 @@
|
||||
"createDomainARecords": "Enregistrements A",
|
||||
"createDomainRecordNumber": "Enregistrement {number}",
|
||||
"createDomainTxtRecords": "Enregistrements TXT",
|
||||
"createDomainSaveTheseRecords": "Sauvegardez ces enregistrements",
|
||||
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
|
||||
"createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.",
|
||||
"createDomainDnsPropagation": "Propagation DNS",
|
||||
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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é",
|
||||
@@ -1445,7 +1431,7 @@
|
||||
"IntervalSeconds": "Intervalle sain",
|
||||
"timeoutSeconds": "Délai",
|
||||
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
||||
"retryAttempts": "Tentatives",
|
||||
"retryAttempts": "Tentatives de réessai",
|
||||
"expectedResponseCodes": "Codes de réponse attendus",
|
||||
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
|
||||
"customHeaders": "En-têtes personnalisés",
|
||||
@@ -1557,8 +1543,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": "Nœuds distants",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gérer auto-hébergé",
|
||||
"remoteExitNodeDescription": "Gérer les nœuds pour étendre votre connectivité réseau",
|
||||
"remoteExitNodes": "Nœuds",
|
||||
"searchRemoteExitNodes": "Rechercher des nœuds...",
|
||||
"remoteExitNodeAdd": "Ajouter un noeud",
|
||||
@@ -1568,7 +1554,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 distants",
|
||||
"sidebarRemoteExitNodes": "Nœuds",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Créer un noeud",
|
||||
"description": "Créer un nouveau nœud pour étendre votre connectivité réseau",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Page d\u000027authentification mise à jour avec succès",
|
||||
"healthCheckNotAvailable": "Locale",
|
||||
"rewritePath": "Réécrire le chemin",
|
||||
"rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible.",
|
||||
"continueToApplication": "Continuer vers l'application",
|
||||
"checkingInvite": "Vérification de l'invitation",
|
||||
"setResourceHeaderAuth": "Définir l\\'authentification d\\'en-tête de la ressource",
|
||||
"resourceHeaderAuthRemove": "Supprimer l'authentification de l'en-tête",
|
||||
"resourceHeaderAuthRemoveDescription": "Authentification de l'en-tête supprimée avec succès.",
|
||||
"resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification de l'en-tête",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification de l'en-tête de la ressource.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification de l'en-tête",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification de l'en-tête pour la ressource.",
|
||||
"resourceHeaderAuthSetup": "Authentification de l'en-tête définie avec succès",
|
||||
"resourceHeaderAuthSetupDescription": "L'authentification de l'en-tête a été définie avec succès.",
|
||||
"resourceHeaderAuthSetupTitle": "Authentification de l'en-tête",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Authentification de l'en-tête",
|
||||
"actionSetResourceHeaderAuth": "Authentification de l'en-tête",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "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@fossorial.io."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteSeeAll": "Vedi Tutti I Siti",
|
||||
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
||||
"siteNewtCredentials": "Credenziali Newt",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Creato Il",
|
||||
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
|
||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||
"proxyEnableSSL": "Abilita SSL",
|
||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configura Obiettivi",
|
||||
"proxyEnableSSL": "Abilita SSL (https)",
|
||||
"targetErrorFetch": "Impossibile recuperare i target",
|
||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||
"siteErrorFetch": "Impossibile recuperare la risorsa",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Configurazione Connessione Sicura",
|
||||
"targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa",
|
||||
"targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate",
|
||||
"targetTlsSni": "Nome Server Tls",
|
||||
"targetTlsSni": "Nome Server TLS (SNI)",
|
||||
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
||||
"targetTlsSubmit": "Salva Impostazioni",
|
||||
"targets": "Configurazione Target",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
||||
"methodSelect": "Seleziona metodo",
|
||||
"targetSubmit": "Aggiungi Target",
|
||||
"targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.",
|
||||
"targetNoOne": "Nessun target. Aggiungi un target usando il modulo.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolina",
|
||||
"licenseTierProfessional": "Licenza Professional",
|
||||
"licenseTierEnterprise": "Licenza Enterprise",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Licenza Commerciale",
|
||||
"licensed": "Con Licenza",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Professionista",
|
||||
"idpType": "Tipo di Provider",
|
||||
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
|
||||
"idpOidcConfigure": "Configurazione OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Tutti Gli Utenti",
|
||||
"sidebarIdentityProviders": "Fornitori Di Identità",
|
||||
"sidebarLicense": "Licenza",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Clienti (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "Proxy Esterno Abilitato",
|
||||
"addNewTarget": "Aggiungi Nuovo Target",
|
||||
"targetsList": "Elenco dei Target",
|
||||
"advancedMode": "Modalità Avanzata",
|
||||
"targetErrorDuplicateTargetFound": "Target duplicato trovato",
|
||||
"healthCheckHealthy": "Sano",
|
||||
"healthCheckUnhealthy": "Non Sano",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Errore di Accesso Automatico",
|
||||
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
||||
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Nodi Remoti",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gestisci Self-Hosted",
|
||||
"remoteExitNodeDescription": "Gestisci i nodi per estendere la connettività di rete",
|
||||
"remoteExitNodes": "Nodi",
|
||||
"searchRemoteExitNodes": "Cerca nodi...",
|
||||
"remoteExitNodeAdd": "Aggiungi Nodo",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.",
|
||||
"remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo",
|
||||
"remoteExitNodeDelete": "Elimina Nodo",
|
||||
"sidebarRemoteExitNodes": "Nodi Remoti",
|
||||
"sidebarRemoteExitNodes": "Nodi",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Crea Nodo",
|
||||
"description": "Crea un nuovo nodo per estendere la connettività di rete",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Pagina di autenticazione aggiornata con successo",
|
||||
"healthCheckNotAvailable": "Locale",
|
||||
"rewritePath": "Riscrivi percorso",
|
||||
"rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target.",
|
||||
"continueToApplication": "Continua con l'applicazione",
|
||||
"checkingInvite": "Controllo Invito",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Rimuovi Autenticazione Intestazione",
|
||||
"resourceHeaderAuthRemoveDescription": "Autenticazione intestazione rimossa con successo.",
|
||||
"resourceErrorHeaderAuthRemove": "Impossibile rimuovere l'autenticazione dell'intestazione",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Impossibile rimuovere l'autenticazione dell'intestazione per la risorsa.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Impossibile impostare l'autenticazione dell'intestazione",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Impossibile impostare l'autenticazione dell'intestazione per la risorsa.",
|
||||
"resourceHeaderAuthSetup": "Autenticazione intestazione impostata con successo",
|
||||
"resourceHeaderAuthSetupDescription": "L'autenticazione dell'intestazione è stata impostata correttamente.",
|
||||
"resourceHeaderAuthSetupTitle": "Imposta Autenticazione Intestazione",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Imposta Autenticazione Intestazione",
|
||||
"actionSetResourceHeaderAuth": "Imposta Autenticazione Intestazione",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priorità",
|
||||
"priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target."
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteSeeAll": "모든 사이트 보기",
|
||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||
"siteNewtCredentials": "Newt 자격 증명",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "생성일",
|
||||
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
|
||||
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
|
||||
"proxyEnableSSL": "SSL 활성화",
|
||||
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.",
|
||||
"target": "대상",
|
||||
"configureTarget": "대상 구성",
|
||||
"proxyEnableSSL": "SSL 활성화 (https)",
|
||||
"targetErrorFetch": "대상 가져오는 데 실패했습니다.",
|
||||
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
|
||||
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "보안 연결 구성",
|
||||
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
|
||||
"targetTlsSettingsAdvanced": "고급 TLS 설정",
|
||||
"targetTlsSni": "TLS 서버 이름",
|
||||
"targetTlsSni": "TLS 서버 이름 (SNI)",
|
||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||
"targetTlsSubmit": "설정 저장",
|
||||
"targets": "대상 구성",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||
"methodSelect": "선택 방법",
|
||||
"targetSubmit": "대상 추가",
|
||||
"targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.",
|
||||
"targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.",
|
||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||
"targetsSubmit": "대상 저장",
|
||||
"addTarget": "대상 추가",
|
||||
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
|
||||
"targetErrorNoSite": "선택된 사이트 없음",
|
||||
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
|
||||
"targetCreated": "대상 생성",
|
||||
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
|
||||
"targetErrorCreate": "대상 생성 실패",
|
||||
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
|
||||
"save": "저장",
|
||||
"proxyAdditional": "추가 프록시 설정",
|
||||
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
|
||||
"proxyCustomHeader": "사용자 정의 호스트 헤더",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "서버 관리자 - 판골린",
|
||||
"licenseTierProfessional": "전문 라이센스",
|
||||
"licenseTierEnterprise": "기업 라이선스",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "상업용 라이선스",
|
||||
"licensed": "라이센스",
|
||||
"yes": "예",
|
||||
"no": "아니요",
|
||||
@@ -765,7 +750,7 @@
|
||||
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
||||
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
||||
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
||||
"licenseBadge": "EE",
|
||||
"licenseBadge": "전문가",
|
||||
"idpType": "제공자 유형",
|
||||
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 구성",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"navbar": "탐색 메뉴",
|
||||
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
|
||||
"navbarDocsLink": "문서",
|
||||
"commercialEdition": "상업용 에디션",
|
||||
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
|
||||
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
|
||||
"otpSetupCheckCode": "6자리 코드를 입력하세요",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "모든 사용자",
|
||||
"sidebarIdentityProviders": "신원 공급자",
|
||||
"sidebarLicense": "라이선스",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "클라이언트 (Beta)",
|
||||
"sidebarDomains": "도메인",
|
||||
"enableDockerSocket": "Docker 청사진 활성화",
|
||||
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
|
||||
"twoFactor": "이중 인증",
|
||||
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
|
||||
"continueToApplication": "응용 프로그램으로 계속",
|
||||
"securityKeyAdd": "보안 키 추가",
|
||||
"securityKeyRegisterTitle": "새 보안 키 등록",
|
||||
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "외부 프록시 활성화됨",
|
||||
"addNewTarget": "새 대상 추가",
|
||||
"targetsList": "대상 목록",
|
||||
"advancedMode": "고급 모드",
|
||||
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
||||
"healthCheckHealthy": "정상",
|
||||
"healthCheckUnhealthy": "비정상",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "자동 로그인 오류",
|
||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "관리 자체 호스팅",
|
||||
"remoteExitNodeDescription": "네트워크 연결성을 확장하기 위해 노드를 관리하세요",
|
||||
"remoteExitNodes": "노드",
|
||||
"searchRemoteExitNodes": "노드 검색...",
|
||||
"remoteExitNodeAdd": "노드 추가",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.",
|
||||
"remoteExitNodeConfirmDelete": "노드 삭제 확인",
|
||||
"remoteExitNodeDelete": "노드 삭제",
|
||||
"sidebarRemoteExitNodes": "원격 노드",
|
||||
"sidebarRemoteExitNodes": "노드",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "노드 생성",
|
||||
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
|
||||
"healthCheckNotAvailable": "로컬",
|
||||
"rewritePath": "경로 재작성",
|
||||
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다.",
|
||||
"continueToApplication": "응용 프로그램으로 계속",
|
||||
"checkingInvite": "초대 확인 중",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "헤더 인증 제거",
|
||||
"resourceHeaderAuthRemoveDescription": "헤더 인증이 성공적으로 제거되었습니다.",
|
||||
"resourceErrorHeaderAuthRemove": "헤더 인증 제거 실패",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "리소스의 헤더 인증을 제거할 수 없습니다.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "헤더 인증 설정 실패",
|
||||
"resourceErrorHeaderAuthSetupDescription": "리소스의 헤더 인증을 설정할 수 없습니다.",
|
||||
"resourceHeaderAuthSetup": "헤더 인증이 성공적으로 설정되었습니다.",
|
||||
"resourceHeaderAuthSetupDescription": "헤더 인증이 성공적으로 설정되었습니다.",
|
||||
"resourceHeaderAuthSetupTitle": "헤더 인증 설정",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "헤더 인증 설정",
|
||||
"actionSetResourceHeaderAuth": "헤더 인증 설정",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "우선순위",
|
||||
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
||||
"siteSeeAll": "Se alle områder",
|
||||
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
||||
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Opprettet",
|
||||
"proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.",
|
||||
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
|
||||
"proxyEnableSSL": "Aktiver SSL",
|
||||
"proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Konfigurer mål",
|
||||
"proxyEnableSSL": "Aktiver SSL (https)",
|
||||
"targetErrorFetch": "Kunne ikke hente mål",
|
||||
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
|
||||
"siteErrorFetch": "Klarte ikke å hente ressurs",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Sikker tilkoblings-konfigurasjon",
|
||||
"targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din",
|
||||
"targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger",
|
||||
"targetTlsSni": "TLS servernavn",
|
||||
"targetTlsSni": "TLS Servernavn (SNI)",
|
||||
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
||||
"targetTlsSubmit": "Lagre innstillinger",
|
||||
"targets": "Målkonfigurasjon",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
||||
"methodSelect": "Velg metode",
|
||||
"targetSubmit": "Legg til mål",
|
||||
"targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.",
|
||||
"targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
"licenseTierProfessional": "Profesjonell lisens",
|
||||
"licenseTierEnterprise": "Bedriftslisens",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Kommersiell lisens",
|
||||
"licensed": "Lisensiert",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profesjonell",
|
||||
"idpType": "Leverandørtype",
|
||||
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
|
||||
"idpOidcConfigure": "OAuth2/OIDC-konfigurasjon",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Alle brukere",
|
||||
"sidebarIdentityProviders": "Identitetsleverandører",
|
||||
"sidebarLicense": "Lisens",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Klienter (Beta)",
|
||||
"sidebarDomains": "Domener",
|
||||
"enableDockerSocket": "Aktiver Docker blåkopi",
|
||||
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Feil ved automatisk innlogging",
|
||||
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
|
||||
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Eksterne Noder",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Administrer Selv-Hostet",
|
||||
"remoteExitNodeDescription": "Administrer noder for å forlenge nettverkstilkoblingen din",
|
||||
"remoteExitNodes": "Noder",
|
||||
"searchRemoteExitNodes": "Søk noder...",
|
||||
"remoteExitNodeAdd": "Legg til Node",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.",
|
||||
"remoteExitNodeConfirmDelete": "Bekreft sletting av Node",
|
||||
"remoteExitNodeDelete": "Slett Node",
|
||||
"sidebarRemoteExitNodes": "Eksterne Noder",
|
||||
"sidebarRemoteExitNodes": "Noder",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Opprett node",
|
||||
"description": "Opprett en ny node for å utvide nettverkstilkoblingen din",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Godkjenningsside oppdatert",
|
||||
"healthCheckNotAvailable": "Lokal",
|
||||
"rewritePath": "Omskriv sti",
|
||||
"rewritePathDescription": "Valgfritt omskrive stien før videresending til målet.",
|
||||
"continueToApplication": "Fortsett til applikasjonen",
|
||||
"checkingInvite": "Sjekker invitasjon",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Fjern topptekst Auth",
|
||||
"resourceHeaderAuthRemoveDescription": "Topplinje autentisering fjernet.",
|
||||
"resourceErrorHeaderAuthRemove": "Kunne ikke fjerne topptekst autentisering",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Kunne ikke fjerne topptekst autentisering for ressursen.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Kunne ikke sette topptekst autentisering",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Kunne ikke sette topplinje autentisering for ressursen.",
|
||||
"resourceHeaderAuthSetup": "Header godkjenningssett var vellykket",
|
||||
"resourceHeaderAuthSetupDescription": "Topplinje autentisering har blitt lagret.",
|
||||
"resourceHeaderAuthSetupTitle": "Angi topptekst godkjenning",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Angi topptekst godkjenning",
|
||||
"actionSetResourceHeaderAuth": "Angi topptekst godkjenning",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Prioritet",
|
||||
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Valgfritt omskrive stien før videresending til målet."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||
"siteSeeAll": "Alle sites bekijken",
|
||||
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
||||
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Aangemaakt op",
|
||||
"proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.",
|
||||
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
|
||||
"proxyEnableSSL": "SSL inschakelen",
|
||||
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Doelstellingen configureren",
|
||||
"proxyEnableSSL": "SSL (https) inschakelen",
|
||||
"targetErrorFetch": "Ophalen van doelen mislukt",
|
||||
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
|
||||
"siteErrorFetch": "Mislukt om resource op te halen",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "HTTPS & TLS instellingen",
|
||||
"targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren",
|
||||
"targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen",
|
||||
"targetTlsSni": "TLS servernaam",
|
||||
"targetTlsSni": "TLS Server Naam (SNI)",
|
||||
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
||||
"targetTlsSubmit": "Instellingen opslaan",
|
||||
"targets": "Doelstellingen configuratie",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||
"methodSelect": "Selecteer methode",
|
||||
"targetSubmit": "Doelwit toevoegen",
|
||||
"targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.",
|
||||
"targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Serverbeheer - Pangolin",
|
||||
"licenseTierProfessional": "Professionele licentie",
|
||||
"licenseTierEnterprise": "Enterprise Licentie",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Commerciële licentie",
|
||||
"licensed": "Gelicentieerd",
|
||||
"yes": "ja",
|
||||
"no": "Neen",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Professioneel",
|
||||
"idpType": "Type provider",
|
||||
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
|
||||
"idpOidcConfigure": "OAuth2/OIDC configuratie",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Alle gebruikers",
|
||||
"sidebarIdentityProviders": "Identiteit aanbieders",
|
||||
"sidebarLicense": "Licentie",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Clients (Bèta)",
|
||||
"sidebarDomains": "Domeinen",
|
||||
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
||||
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "Externe Proxy Ingeschakeld",
|
||||
"addNewTarget": "Voeg nieuw doelwit toe",
|
||||
"targetsList": "Lijst met doelen",
|
||||
"advancedMode": "Geavanceerde modus",
|
||||
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
|
||||
"healthCheckHealthy": "Gezond",
|
||||
"healthCheckUnhealthy": "Ongezond",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Auto Login Fout",
|
||||
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Externe knooppunten",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Beheer Zelf-Gehoste",
|
||||
"remoteExitNodeDescription": "Beheer knooppunten om uw netwerkverbinding uit te breiden",
|
||||
"remoteExitNodes": "Nodes",
|
||||
"searchRemoteExitNodes": "Knooppunten zoeken...",
|
||||
"remoteExitNodeAdd": "Voeg node toe",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.",
|
||||
"remoteExitNodeConfirmDelete": "Bevestig verwijderen node",
|
||||
"remoteExitNodeDelete": "Knoop verwijderen",
|
||||
"sidebarRemoteExitNodes": "Externe knooppunten",
|
||||
"sidebarRemoteExitNodes": "Nodes",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Maak node",
|
||||
"description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Auth-pagina succesvol bijgewerkt",
|
||||
"healthCheckNotAvailable": "Lokaal",
|
||||
"rewritePath": "Herschrijf Pad",
|
||||
"rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt.",
|
||||
"continueToApplication": "Doorgaan naar applicatie",
|
||||
"checkingInvite": "Uitnodiging controleren",
|
||||
"setResourceHeaderAuth": "stelResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Auth koptekst verwijderen",
|
||||
"resourceHeaderAuthRemoveDescription": "Koptekst authenticatie succesvol verwijderd.",
|
||||
"resourceErrorHeaderAuthRemove": "Kan Header-authenticatie niet verwijderen",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Kon header authenticatie niet verwijderen voor de bron.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Kan Header Authenticatie niet instellen",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Kan geen header authenticatie instellen voor de bron.",
|
||||
"resourceHeaderAuthSetup": "Header Authenticatie set succesvol",
|
||||
"resourceHeaderAuthSetupDescription": "Header authenticatie is met succes ingesteld.",
|
||||
"resourceHeaderAuthSetupTitle": "Header Authenticatie instellen",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Header Authenticatie instellen",
|
||||
"actionSetResourceHeaderAuth": "Header Authenticatie instellen",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Prioriteit",
|
||||
"priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||
"siteSeeAll": "Zobacz wszystkie witryny",
|
||||
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
||||
"siteNewtCredentials": "Aktualne dane logowania",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Utworzono",
|
||||
"proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.",
|
||||
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
|
||||
"proxyEnableSSL": "Włącz SSL",
|
||||
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Konfiguruj Targety",
|
||||
"proxyEnableSSL": "Włącz SSL (https)",
|
||||
"targetErrorFetch": "Nie udało się pobrać celów",
|
||||
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
|
||||
"siteErrorFetch": "Nie udało się pobrać zasobu",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Konfiguracja bezpiecznego połączenia",
|
||||
"targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu",
|
||||
"targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS",
|
||||
"targetTlsSni": "Nazwa serwera TLS",
|
||||
"targetTlsSni": "Nazwa serwera TLS (SNI)",
|
||||
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
||||
"targetTlsSubmit": "Zapisz ustawienia",
|
||||
"targets": "Konfiguracja celów",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
||||
"methodSelect": "Wybierz metodę",
|
||||
"targetSubmit": "Dodaj cel",
|
||||
"targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.",
|
||||
"targetNoOne": "Brak celów. Dodaj cel używając formularza.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Administrator serwera - Pangolin",
|
||||
"licenseTierProfessional": "Licencja Professional",
|
||||
"licenseTierEnterprise": "Licencja Enterprise",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Licencja handlowa",
|
||||
"licensed": "Licencjonowany",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profesjonalny",
|
||||
"idpType": "Typ dostawcy",
|
||||
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
|
||||
"idpOidcConfigure": "Konfiguracja OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Wszyscy użytkownicy",
|
||||
"sidebarIdentityProviders": "Dostawcy tożsamości",
|
||||
"sidebarLicense": "Licencja",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Klienci (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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ć",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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",
|
||||
@@ -1557,8 +1543,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": "Zdalne węzły",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Zarządzaj Samodzielnie-Hostingowane",
|
||||
"remoteExitNodeDescription": "Zarządzaj węzłami w celu rozszerzenia połączenia z siecią",
|
||||
"remoteExitNodes": "Węzły",
|
||||
"searchRemoteExitNodes": "Szukaj węzłów...",
|
||||
"remoteExitNodeAdd": "Dodaj węzeł",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.",
|
||||
"remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła",
|
||||
"remoteExitNodeDelete": "Usuń węzeł",
|
||||
"sidebarRemoteExitNodes": "Zdalne węzły",
|
||||
"sidebarRemoteExitNodes": "Węzły",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Utwórz węzeł",
|
||||
"description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana",
|
||||
"healthCheckNotAvailable": "Lokalny",
|
||||
"rewritePath": "Przepis Ścieżki",
|
||||
"rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu.",
|
||||
"continueToApplication": "Kontynuuj do aplikacji",
|
||||
"checkingInvite": "Sprawdzanie zaproszenia",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Usuń autoryzację nagłówka",
|
||||
"resourceHeaderAuthRemoveDescription": "Uwierzytelnianie nagłówka zostało pomyślnie usunięte.",
|
||||
"resourceErrorHeaderAuthRemove": "Nie udało się usunąć uwierzytelniania nagłówka",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Nie można usunąć uwierzytelniania nagłówka zasobu.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Nie udało się ustawić uwierzytelniania nagłówka",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Nie można ustawić uwierzytelniania nagłówka dla zasobu.",
|
||||
"resourceHeaderAuthSetup": "Uwierzytelnianie nagłówka ustawione pomyślnie",
|
||||
"resourceHeaderAuthSetupDescription": "Uwierzytelnianie nagłówka zostało ustawione.",
|
||||
"resourceHeaderAuthSetupTitle": "Ustaw uwierzytelnianie nagłówka",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Ustaw uwierzytelnianie nagłówka",
|
||||
"actionSetResourceHeaderAuth": "Ustaw uwierzytelnianie nagłówka",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priorytet",
|
||||
"priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||
"siteSeeAll": "Ver todos os sites",
|
||||
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
||||
"siteNewtCredentials": "Credenciais Novas",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Criado Em",
|
||||
"proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.",
|
||||
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
|
||||
"proxyEnableSSL": "Habilitar SSL",
|
||||
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Configurar Alvos",
|
||||
"proxyEnableSSL": "Habilitar SSL (https)",
|
||||
"targetErrorFetch": "Falha ao buscar alvos",
|
||||
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
|
||||
"siteErrorFetch": "Falha ao buscar recurso",
|
||||
@@ -498,7 +495,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",
|
||||
"targetTlsSni": "Nome do Servidor TLS (SNI)",
|
||||
"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",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
||||
"methodSelect": "Selecionar método",
|
||||
"targetSubmit": "Adicionar Alvo",
|
||||
"targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.",
|
||||
"targetNoOne": "Sem alvos. Adicione um alvo usando o formulário.",
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
|
||||
"licenseTierProfessional": "Licença Profissional",
|
||||
"licenseTierEnterprise": "Licença Empresarial",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Licença comercial",
|
||||
"licensed": "Licenciado",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profissional",
|
||||
"idpType": "Tipo de Provedor",
|
||||
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
|
||||
"idpOidcConfigure": "Configuração OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Todos os utilizadores",
|
||||
"sidebarIdentityProviders": "Provedores de identidade",
|
||||
"sidebarLicense": "Tipo:",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Clientes (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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",
|
||||
@@ -1557,8 +1543,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": "Nós remotos",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Gerenciar Auto-Hospedados",
|
||||
"remoteExitNodeDescription": "Gerencie os nós para estender sua conectividade de rede",
|
||||
"remoteExitNodes": "Nós",
|
||||
"searchRemoteExitNodes": "Buscar nós...",
|
||||
"remoteExitNodeAdd": "Adicionar node",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.",
|
||||
"remoteExitNodeConfirmDelete": "Confirmar exclusão do nó",
|
||||
"remoteExitNodeDelete": "Excluir nó",
|
||||
"sidebarRemoteExitNodes": "Nós remotos",
|
||||
"sidebarRemoteExitNodes": "Nós",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Criar nó",
|
||||
"description": "Crie um novo nó para estender sua conectividade de rede",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Página de autenticação atualizada com sucesso",
|
||||
"healthCheckNotAvailable": "Localização",
|
||||
"rewritePath": "Reescrever Caminho",
|
||||
"rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino.",
|
||||
"continueToApplication": "Continuar para o aplicativo",
|
||||
"checkingInvite": "Checando convite",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Remover autenticação de cabeçalho",
|
||||
"resourceHeaderAuthRemoveDescription": "Autenticação de cabeçalho removida com sucesso.",
|
||||
"resourceErrorHeaderAuthRemove": "Falha ao remover autenticação de cabeçalho",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Não foi possível remover a autenticação do cabeçalho para o recurso.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Falha ao definir autenticação de cabeçalho",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Não foi possível definir a autenticação do cabeçalho para o recurso.",
|
||||
"resourceHeaderAuthSetup": "Autenticação de Cabeçalho definida com sucesso",
|
||||
"resourceHeaderAuthSetupDescription": "Autenticação de cabeçalho foi definida com sucesso.",
|
||||
"resourceHeaderAuthSetupTitle": "Definir autenticação de cabeçalho",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Definir autenticação de cabeçalho",
|
||||
"actionSetResourceHeaderAuth": "Definir autenticação de cabeçalho",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Prioridade",
|
||||
"priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino."
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
||||
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||
"siteSeeAll": "Просмотреть все сайты",
|
||||
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
||||
"siteNewtCredentials": "Учётные данные Newt",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Создано в",
|
||||
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
|
||||
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
|
||||
"proxyEnableSSL": "Включить SSL",
|
||||
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.",
|
||||
"target": "Target",
|
||||
"configureTarget": "Настроить адресаты",
|
||||
"proxyEnableSSL": "Включить SSL (https)",
|
||||
"targetErrorFetch": "Не удалось получить цели",
|
||||
"targetErrorFetchDescription": "Произошла ошибка при получении целей",
|
||||
"siteErrorFetch": "Не удалось получить ресурс",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "Конфигурация безопасного соединения",
|
||||
"targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса",
|
||||
"targetTlsSettingsAdvanced": "Расширенные настройки TLS",
|
||||
"targetTlsSni": "Имя TLS сервера",
|
||||
"targetTlsSni": "Имя TLS сервера (SNI)",
|
||||
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
||||
"targetTlsSubmit": "Сохранить настройки",
|
||||
"targets": "Конфигурация целей",
|
||||
@@ -507,21 +504,9 @@
|
||||
"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",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Администратор сервера - Pangolin",
|
||||
"licenseTierProfessional": "Профессиональная лицензия",
|
||||
"licenseTierEnterprise": "Корпоративная лицензия",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Коммерческая лицензия",
|
||||
"licensed": "Лицензировано",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
@@ -765,7 +750,7 @@
|
||||
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
|
||||
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
|
||||
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
|
||||
"licenseBadge": "EE",
|
||||
"licenseBadge": "Профессиональная",
|
||||
"idpType": "Тип поставщика",
|
||||
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
|
||||
"idpOidcConfigure": "Конфигурация OAuth2/OIDC",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"navbar": "Навигационное меню",
|
||||
"navbarDescription": "Главное навигационное меню приложения",
|
||||
"navbarDocsLink": "Документация",
|
||||
"commercialEdition": "Коммерческая версия",
|
||||
"otpErrorEnable": "Невозможно включить 2FA",
|
||||
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
|
||||
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Все пользователи",
|
||||
"sidebarIdentityProviders": "Поставщики удостоверений",
|
||||
"sidebarLicense": "Лицензия",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Клиенты (бета)",
|
||||
"sidebarDomains": "Домены",
|
||||
"enableDockerSocket": "Включить чертёж Docker",
|
||||
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
|
||||
"twoFactor": "Двухфакторная аутентификация",
|
||||
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
|
||||
"continueToApplication": "Перейти к приложению",
|
||||
"securityKeyAdd": "Добавить ключ безопасности",
|
||||
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
|
||||
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "Внешний прокси включен",
|
||||
"addNewTarget": "Добавить новую цель",
|
||||
"targetsList": "Список целей",
|
||||
"advancedMode": "Расширенный режим",
|
||||
"targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель",
|
||||
"healthCheckHealthy": "Здоровый",
|
||||
"healthCheckUnhealthy": "Нездоровый",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "Ошибка автоматического входа",
|
||||
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Удаленные узлы",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Управление самоуправляемым",
|
||||
"remoteExitNodeDescription": "Управляйте узлами для расширения сетевого подключения",
|
||||
"remoteExitNodes": "Узлы",
|
||||
"searchRemoteExitNodes": "Поиск узлов...",
|
||||
"remoteExitNodeAdd": "Добавить узел",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.",
|
||||
"remoteExitNodeConfirmDelete": "Подтвердите удаление узла",
|
||||
"remoteExitNodeDelete": "Удалить узел",
|
||||
"sidebarRemoteExitNodes": "Удаленные узлы",
|
||||
"sidebarRemoteExitNodes": "Узлы",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Создать узел",
|
||||
"description": "Создайте новый узел, чтобы расширить сетевое подключение",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Страница авторизации успешно обновлена",
|
||||
"healthCheckNotAvailable": "Локальный",
|
||||
"rewritePath": "Переписать путь",
|
||||
"rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу.",
|
||||
"continueToApplication": "Перейти к приложению",
|
||||
"checkingInvite": "Проверка приглашения",
|
||||
"setResourceHeaderAuth": "установить заголовок ресурса",
|
||||
"resourceHeaderAuthRemove": "Удалить проверку подлинности заголовка",
|
||||
"resourceHeaderAuthRemoveDescription": "Проверка подлинности заголовка успешно удалена.",
|
||||
"resourceErrorHeaderAuthRemove": "Не удалось удалить аутентификацию заголовка",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Не удалось удалить проверку подлинности заголовка ресурса.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Не удалось установить аутентификацию заголовка",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Не удалось установить проверку подлинности заголовка ресурса.",
|
||||
"resourceHeaderAuthSetup": "Проверка подлинности заголовка успешно установлена",
|
||||
"resourceHeaderAuthSetupDescription": "Проверка подлинности заголовка успешно установлена.",
|
||||
"resourceHeaderAuthSetupTitle": "Установить проверку подлинности заголовка",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Установить проверку подлинности заголовка",
|
||||
"actionSetResourceHeaderAuth": "Установить проверку подлинности заголовка",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Приоритет",
|
||||
"priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу."
|
||||
}
|
||||
|
||||
@@ -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": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteSeeAll": "Tüm Siteleri Gör",
|
||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "Oluşturulma Tarihi",
|
||||
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
|
||||
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
|
||||
"proxyEnableSSL": "SSL Etkinleştir",
|
||||
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
|
||||
"target": "Hedef",
|
||||
"configureTarget": "Hedefleri Yapılandır",
|
||||
"proxyEnableSSL": "SSL'yi Etkinleştir (https)",
|
||||
"targetErrorFetch": "Hedefleri alamadı",
|
||||
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
|
||||
"siteErrorFetch": "kaynağa ulaşılamadı",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "HTTPS & TLS Settings",
|
||||
"targetTlsSettingsDescription": "Configure TLS settings for your resource",
|
||||
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
|
||||
"targetTlsSni": "TLS Sunucu Adı",
|
||||
"targetTlsSni": "TLS Sunucu Adı (SNI)",
|
||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||
"targetTlsSubmit": "Ayarları Kaydet",
|
||||
"targets": "Hedefler Konfigürasyonu",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||
"methodSelect": "Yöntemi Seç",
|
||||
"targetSubmit": "Hedef Ekle",
|
||||
"targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
|
||||
"targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir 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ığı",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
|
||||
"licenseTierProfessional": "Profesyonel Lisans",
|
||||
"licenseTierEnterprise": "Kurumsal Lisans",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "Ticari Lisans",
|
||||
"licensed": "Lisanslı",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
@@ -765,7 +750,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": "EE",
|
||||
"licenseBadge": "Profesyonel",
|
||||
"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ı",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"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",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "Tüm Kullanıcılar",
|
||||
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
|
||||
"sidebarLicense": "Lisans",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "Müşteriler (Beta)",
|
||||
"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.",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"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",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"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",
|
||||
@@ -1557,8 +1543,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": "Uzak Düğümler",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Öz-Host Yönetim",
|
||||
"remoteExitNodeDescription": "Ağ bağlantınızı genişletmek için düğümleri yönetin",
|
||||
"remoteExitNodes": "Düğümler",
|
||||
"searchRemoteExitNodes": "Düğüm ara...",
|
||||
"remoteExitNodeAdd": "Düğüm Ekle",
|
||||
@@ -1568,7 +1554,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": "Uzak Düğümler",
|
||||
"sidebarRemoteExitNodes": "Düğümler",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Düğüm Oluştur",
|
||||
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
|
||||
"healthCheckNotAvailable": "Yerel",
|
||||
"rewritePath": "Yolu Yeniden Yaz",
|
||||
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın.",
|
||||
"continueToApplication": "Uygulamaya Devam Et",
|
||||
"checkingInvite": "Davet Kontrol Ediliyor",
|
||||
"setResourceHeaderAuth": "setResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "Başlık Kimlik Doğrulama Kaldır",
|
||||
"resourceHeaderAuthRemoveDescription": "Başlık kimlik doğrulama başarıyla kaldırıldı.",
|
||||
"resourceErrorHeaderAuthRemove": "Başlık Kimlik Doğrulama kaldırılamadı",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "Kaynak için başlık kimlik doğrulaması kaldırılamadı.",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "Başlık Kimlik Doğrulama ayarlanamadı",
|
||||
"resourceErrorHeaderAuthSetupDescription": "Kaynak için başlık kimlik doğrulaması ayarlanamadı.",
|
||||
"resourceHeaderAuthSetup": "Başlık Kimlik Doğrulama başarıyla ayarlandı",
|
||||
"resourceHeaderAuthSetupDescription": "Başlık kimlik doğrulaması başarıyla ayarlandı.",
|
||||
"resourceHeaderAuthSetupTitle": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"actionSetResourceHeaderAuth": "Başlık Kimlik Doğrulama Ayarla",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Öncelik",
|
||||
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın."
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
||||
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。",
|
||||
"siteSeeAll": "查看所有站点",
|
||||
"siteTunnelDescription": "确定如何连接到您的网站",
|
||||
"siteNewtCredentials": "Newt 凭据",
|
||||
@@ -468,10 +468,7 @@
|
||||
"createdAt": "创建于",
|
||||
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
|
||||
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
|
||||
"proxyEnableSSL": "启用 SSL",
|
||||
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。",
|
||||
"target": "Target",
|
||||
"configureTarget": "配置目标",
|
||||
"proxyEnableSSL": "启用 SSL (https)",
|
||||
"targetErrorFetch": "获取目标失败",
|
||||
"targetErrorFetchDescription": "获取目标时出错",
|
||||
"siteErrorFetch": "获取资源失败",
|
||||
@@ -498,7 +495,7 @@
|
||||
"targetTlsSettings": "安全连接配置",
|
||||
"targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置",
|
||||
"targetTlsSettingsAdvanced": "高级TLS设置",
|
||||
"targetTlsSni": "TLS 服务器名称",
|
||||
"targetTlsSni": "TLS 服务器名称 (SNI)",
|
||||
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
||||
"targetTlsSubmit": "保存设置",
|
||||
"targets": "目标配置",
|
||||
@@ -507,21 +504,9 @@
|
||||
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
||||
"methodSelect": "选择方法",
|
||||
"targetSubmit": "添加目标",
|
||||
"targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。",
|
||||
"targetNoOne": "没有目标。使用表单添加目标。",
|
||||
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
||||
"targetsSubmit": "保存目标",
|
||||
"addTarget": "添加目标",
|
||||
"targetErrorInvalidIp": "无效的 IP 地址",
|
||||
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
||||
"targetErrorInvalidPort": "无效的端口",
|
||||
"targetErrorInvalidPortDescription": "请输入有效的端口号",
|
||||
"targetErrorNoSite": "没有选择站点",
|
||||
"targetErrorNoSiteDescription": "请选择目标站点",
|
||||
"targetCreated": "目标已创建",
|
||||
"targetCreatedDescription": "目标已成功创建",
|
||||
"targetErrorCreate": "创建目标失败",
|
||||
"targetErrorCreateDescription": "创建目标时出错",
|
||||
"save": "保存",
|
||||
"proxyAdditional": "附加代理设置",
|
||||
"proxyAdditionalDescription": "配置你的资源如何处理代理设置",
|
||||
"proxyCustomHeader": "自定义主机标题",
|
||||
@@ -730,7 +715,7 @@
|
||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||
"licenseTierProfessional": "专业许可证",
|
||||
"licenseTierEnterprise": "企业许可证",
|
||||
"licenseTierPersonal": "Personal License",
|
||||
"licenseTierCommercial": "商业许可证",
|
||||
"licensed": "已授权",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
@@ -765,7 +750,7 @@
|
||||
"idpDisplayName": "此身份提供商的显示名称",
|
||||
"idpAutoProvisionUsers": "自动提供用户",
|
||||
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
||||
"licenseBadge": "EE",
|
||||
"licenseBadge": "专业版",
|
||||
"idpType": "提供者类型",
|
||||
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 配置",
|
||||
@@ -1099,6 +1084,7 @@
|
||||
"navbar": "导航菜单",
|
||||
"navbarDescription": "应用程序的主导航菜单",
|
||||
"navbarDocsLink": "文件",
|
||||
"commercialEdition": "商业版",
|
||||
"otpErrorEnable": "无法启用 2FA",
|
||||
"otpErrorEnableDescription": "启用 2FA 时出错",
|
||||
"otpSetupCheckCode": "请输入您的6位数字代码",
|
||||
@@ -1154,7 +1140,7 @@
|
||||
"sidebarAllUsers": "所有用户",
|
||||
"sidebarIdentityProviders": "身份提供商",
|
||||
"sidebarLicense": "证书",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarClients": "客户端(测试版)",
|
||||
"sidebarDomains": "域",
|
||||
"enableDockerSocket": "启用 Docker 蓝图",
|
||||
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
||||
@@ -1347,6 +1333,7 @@
|
||||
"twoFactorRequired": "注册安全密钥需要两步验证。",
|
||||
"twoFactor": "两步验证",
|
||||
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
|
||||
"continueToApplication": "继续应用",
|
||||
"securityKeyAdd": "添加安全密钥",
|
||||
"securityKeyRegisterTitle": "注册新安全密钥",
|
||||
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
|
||||
@@ -1424,7 +1411,6 @@
|
||||
"externalProxyEnabled": "外部代理已启用",
|
||||
"addNewTarget": "添加新目标",
|
||||
"targetsList": "目标列表",
|
||||
"advancedMode": "高级模式",
|
||||
"targetErrorDuplicateTargetFound": "找到重复的目标",
|
||||
"healthCheckHealthy": "正常",
|
||||
"healthCheckUnhealthy": "不正常",
|
||||
@@ -1557,8 +1543,8 @@
|
||||
"autoLoginError": "自动登录错误",
|
||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
||||
"remoteExitNodeManageRemoteExitNodes": "远程节点",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeManageRemoteExitNodes": "管理自托管",
|
||||
"remoteExitNodeDescription": "管理节点以扩展您的网络连接",
|
||||
"remoteExitNodes": "节点",
|
||||
"searchRemoteExitNodes": "搜索节点...",
|
||||
"remoteExitNodeAdd": "添加节点",
|
||||
@@ -1568,7 +1554,7 @@
|
||||
"remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。",
|
||||
"remoteExitNodeConfirmDelete": "确认删除节点",
|
||||
"remoteExitNodeDelete": "删除节点",
|
||||
"sidebarRemoteExitNodes": "远程节点",
|
||||
"sidebarRemoteExitNodes": "节点",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "创建节点",
|
||||
"description": "创建一个新节点来扩展您的网络连接",
|
||||
@@ -1737,161 +1723,5 @@
|
||||
"authPageUpdated": "身份验证页面更新成功",
|
||||
"healthCheckNotAvailable": "本地的",
|
||||
"rewritePath": "重写路径",
|
||||
"rewritePathDescription": "在转发到目标之前,可以选择重写路径。",
|
||||
"continueToApplication": "继续应用",
|
||||
"checkingInvite": "正在检查邀请",
|
||||
"setResourceHeaderAuth": "设置 ResourceHeaderAuth",
|
||||
"resourceHeaderAuthRemove": "删除头部认证",
|
||||
"resourceHeaderAuthRemoveDescription": "已成功删除头部身份验证。",
|
||||
"resourceErrorHeaderAuthRemove": "删除头部身份验证失败",
|
||||
"resourceErrorHeaderAuthRemoveDescription": "无法删除资源的头部身份验证。",
|
||||
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
|
||||
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
|
||||
"headerAuthRemove": "Remove Header Auth",
|
||||
"headerAuthAdd": "Add Header Auth",
|
||||
"resourceErrorHeaderAuthSetup": "设置页眉认证失败",
|
||||
"resourceErrorHeaderAuthSetupDescription": "无法设置资源的头部身份验证。",
|
||||
"resourceHeaderAuthSetup": "头部认证设置成功",
|
||||
"resourceHeaderAuthSetupDescription": "头部认证已成功设置。",
|
||||
"resourceHeaderAuthSetupTitle": "设置头部身份验证",
|
||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "设置头部身份验证",
|
||||
"actionSetResourceHeaderAuth": "设置头部身份验证",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
"generateLicenseKey": "Generate License Key",
|
||||
"generateLicenseKeyForm": {
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
"stateProvinceRegionRequired": "State/Province/Region is required",
|
||||
"postalZipCodeRequired": "Postal/ZIP Code is required",
|
||||
"companyNameRequiredBusiness": "Company name is required for business use",
|
||||
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
|
||||
"countryRequiredPersonal": "Country is required for personal use",
|
||||
"agreeToTermsRequired": "You must agree to the terms",
|
||||
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
"title": "Business Use",
|
||||
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"emailLicenseType": {
|
||||
"title": "Email & License Type",
|
||||
"description": "Enter your email and choose your license type"
|
||||
},
|
||||
"personalInformation": {
|
||||
"title": "Personal Information",
|
||||
"description": "Tell us about yourself"
|
||||
},
|
||||
"contactInformation": {
|
||||
"title": "Contact Information",
|
||||
"description": "Your contact details"
|
||||
},
|
||||
"termsGenerate": {
|
||||
"title": "Terms & Generate",
|
||||
"description": "Review and accept terms to generate your license"
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
"success": {
|
||||
"title": "License key generated successfully",
|
||||
"description": "Your license key has been generated and is ready to use."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to generate license key",
|
||||
"description": "An error occurred while generating the license key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "优先权",
|
||||
"priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。",
|
||||
"instanceName": "Instance Name",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
"pathMatchExactHelp": "Example: /api matches only /api",
|
||||
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
|
||||
"pathRewriteModalTitle": "Configure Path Rewriting",
|
||||
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
|
||||
"pathRewriteType": "Rewrite Type",
|
||||
"pathRewritePrefixOption": "Prefix - Replace prefix",
|
||||
"pathRewriteExactOption": "Exact - Replace entire path",
|
||||
"pathRewriteRegexOption": "Regex - Pattern replacement",
|
||||
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
|
||||
"pathRewriteValue": "Rewrite Value",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"rewritePathDescription": "在转发到目标之前,可以选择重写路径。"
|
||||
}
|
||||
|
||||
5719
package-lock.json
generated
5719
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 && 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: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: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"
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
||||
"db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||
"@aws-sdk/client-s3": "3.908.0",
|
||||
"@aws-sdk/client-s3": "3.837.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.6",
|
||||
"@react-email/render": "^1.3.2",
|
||||
"@react-email/components": "0.5.5",
|
||||
"@react-email/render": "^1.2.0",
|
||||
"@react-email/tailwind": "1.2.2",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@simplewebauthn/browser": "^13.2.0",
|
||||
"@simplewebauthn/server": "^13.2.1",
|
||||
"@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.37.0",
|
||||
"eslint-config-next": "15.5.5",
|
||||
"eslint": "9.35.0",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"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.8.1",
|
||||
"ioredis": "5.6.1",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.545.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"maxmind": "5.0.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.5",
|
||||
"next-intl": "^4.3.12",
|
||||
"next": "15.5.4",
|
||||
"next-intl": "^4.3.9",
|
||||
"next-themes": "0.4.6",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.9",
|
||||
"npm": "^11.6.2",
|
||||
"nodemailer": "7.0.6",
|
||||
"npm": "^11.6.1",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.9.5",
|
||||
"posthog-node": "^5.8.4",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-easy-sort": "^1.8.0",
|
||||
"react-hook-form": "7.65.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-easy-sort": "^1.7.0",
|
||||
"react-hook-form": "7.62.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"reodotdev": "^1.0.0",
|
||||
"resend": "^6.1.2",
|
||||
"semver": "^7.7.3",
|
||||
"resend": "^6.1.1",
|
||||
"semver": "^7.7.2",
|
||||
"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.18.3",
|
||||
"winston": "3.17.0",
|
||||
"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.3.0",
|
||||
"@react-email/preview-server": "4.2.12",
|
||||
"@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.7.2",
|
||||
"@types/node": "24.6.2",
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"@types/pg": "8.15.5",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/react": "19.1.16",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@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.11",
|
||||
"esbuild": "0.25.10",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.3.0",
|
||||
"react-email": "4.2.12",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.46.0"
|
||||
"typescript-eslint": "^8.45.0"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 33 KiB |
@@ -7,21 +7,21 @@ import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
|
||||
import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage";
|
||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||
import helmet from "helmet";
|
||||
import { 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 "#dynamic/lib/rateLimitStore";
|
||||
import { createStore } from "@server/lib/private/rateLimitStore";
|
||||
import hybridRouter from "@server/routers/private/hybrid";
|
||||
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,30 +39,32 @@ export function createApiServer() {
|
||||
apiServer.post(
|
||||
`${prefix}/billing/webhooks`,
|
||||
express.raw({ type: "application/json" }),
|
||||
billingWebhookHandler
|
||||
stripeWebhookHandler
|
||||
);
|
||||
}
|
||||
|
||||
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" || !corsConfig) {
|
||||
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)
|
||||
};
|
||||
|
||||
logger.debug("Using CORS options", options);
|
||||
|
||||
apiServer.use(cors(options));
|
||||
} else if (corsConfig) {
|
||||
} else {
|
||||
// Use the custom CORS middleware with loginPage support
|
||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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",
|
||||
@@ -60,7 +61,6 @@ export enum ActionsEnum {
|
||||
getUser = "getUser",
|
||||
setResourcePassword = "setResourcePassword",
|
||||
setResourcePincode = "setResourcePincode",
|
||||
setResourceHeaderAuth = "setResourceHeaderAuth",
|
||||
setResourceWhitelist = "setResourceWhitelist",
|
||||
getResourceWhitelist = "getResourceWhitelist",
|
||||
generateAccessToken = "generateAccessToken",
|
||||
@@ -194,6 +194,7 @@ export async function checkUserActionPermission(
|
||||
|
||||
return roleActionPermission.length > 0;
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking user action permission:", error);
|
||||
throw createHttpError(
|
||||
|
||||
@@ -4,6 +4,9 @@ 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;
|
||||
@@ -62,6 +65,29 @@ 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))
|
||||
);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { cleanup as wsCleanup } from "@server/routers/ws";
|
||||
|
||||
async function cleanup() {
|
||||
await wsCleanup();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export async function initCleanup() {
|
||||
// Handle process termination
|
||||
process.on("SIGTERM", () => cleanup());
|
||||
process.on("SIGINT", () => cleanup());
|
||||
}
|
||||
@@ -35,12 +35,11 @@ function createDb() {
|
||||
}
|
||||
|
||||
// Create connection pools instead of individual connections
|
||||
const poolConfig = config.postgres.pool;
|
||||
const primaryPool = new Pool({
|
||||
connectionString,
|
||||
max: poolConfig?.max_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
const replicas = [];
|
||||
@@ -51,9 +50,9 @@ function createDb() {
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = new Pool({
|
||||
connectionString: conn.connection_string,
|
||||
max: poolConfig?.max_replica_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
replicas.push(DrizzlePostgres(replicaPool));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./schema/schema";
|
||||
export * from "./schema/privateSchema";
|
||||
export * from "./schema";
|
||||
export * from "./privateSchema";
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* 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").default(100)
|
||||
priority: integer("priority").notNull().default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
@@ -381,14 +381,6 @@ 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")
|
||||
@@ -474,6 +466,8 @@ 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
|
||||
@@ -481,6 +475,40 @@ 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(),
|
||||
@@ -698,7 +726,6 @@ 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>;
|
||||
@@ -721,4 +748,6 @@ 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 IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import redisManager from "#private/lib/redis";
|
||||
import redisManager from "@server/db/private/redis";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Rate limiting configuration
|
||||
@@ -451,4 +451,8 @@ export class RateLimitService {
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const rateLimitService = new RateLimitService();
|
||||
export const rateLimitService = new RateLimitService();
|
||||
|
||||
// Handle process termination
|
||||
process.on("SIGTERM", () => rateLimitService.cleanup());
|
||||
process.on("SIGINT", () => rateLimitService.cleanup());
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import Redis, { RedisOptions } from "ioredis";
|
||||
import logger from "@server/logger";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "@server/lib/config";
|
||||
import { build } from "@server/build";
|
||||
|
||||
class RedisManager {
|
||||
@@ -46,7 +46,7 @@ class RedisManager {
|
||||
this.isEnabled = false;
|
||||
return;
|
||||
}
|
||||
this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
|
||||
this.isEnabled = config.getRawPrivateConfig().flags?.enable_redis || false;
|
||||
if (this.isEnabled) {
|
||||
this.initializeClients();
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class RedisManager {
|
||||
}
|
||||
|
||||
private getRedisConfig(): RedisOptions {
|
||||
const redisConfig = privateConfig.getRawPrivateConfig().redis!;
|
||||
const redisConfig = config.getRawPrivateConfig().redis!;
|
||||
const opts: RedisOptions = {
|
||||
host: redisConfig.host!,
|
||||
port: redisConfig.port!,
|
||||
@@ -108,7 +108,7 @@ class RedisManager {
|
||||
}
|
||||
|
||||
private getReplicaRedisConfig(): RedisOptions | null {
|
||||
const redisConfig = privateConfig.getRawPrivateConfig().redis!;
|
||||
const redisConfig = config.getRawPrivateConfig().redis!;
|
||||
if (!redisConfig.replicas || redisConfig.replicas.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
ResourceRule,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceHeaderAuth,
|
||||
ResourceHeaderAuth,
|
||||
resourceRules,
|
||||
resources,
|
||||
roleResources,
|
||||
@@ -17,12 +15,15 @@ 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 = {
|
||||
@@ -36,6 +37,30 @@ 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)
|
||||
@@ -47,10 +72,6 @@ export async function getResourceByDomain(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -61,8 +82,7 @@ export async function getResourceByDomain(
|
||||
return {
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth
|
||||
password: result.resourcePassword
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,6 +92,30 @@ 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)
|
||||
@@ -92,6 +136,30 @@ 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)
|
||||
@@ -108,6 +176,30 @@ 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)
|
||||
@@ -129,6 +221,30 @@ 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)
|
||||
@@ -149,6 +265,30 @@ 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)
|
||||
@@ -163,6 +303,30 @@ 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/schema";
|
||||
import * as schema from "./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/schema";
|
||||
export * from "./schema/privateSchema";
|
||||
export * from "./schema";
|
||||
export * from "./privateSchema";
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* 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").default(100)
|
||||
priority: integer("priority").notNull().default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
@@ -514,16 +514,6 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
||||
passwordHash: text("passwordHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
||||
headerAuthId: integer("headerAuthId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
headerAuthHash: text("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||
accessTokenId: text("accessTokenId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
@@ -610,6 +600,8 @@ 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
|
||||
@@ -617,6 +609,40 @@ 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(),
|
||||
@@ -739,7 +765,6 @@ 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>;
|
||||
@@ -760,4 +785,6 @@ 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 IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>;
|
||||
export type TemplateRule = InferSelectModel<typeof templateRules>;
|
||||
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;
|
||||
@@ -6,6 +6,11 @@ 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,6 +2,7 @@ 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,
|
||||
@@ -24,7 +25,7 @@ export async function sendEmail(
|
||||
|
||||
const emailHtml = await render(template);
|
||||
|
||||
const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
|
||||
const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin";
|
||||
|
||||
await emailClient.sendMail({
|
||||
from: {
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* 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,3 +1,16 @@
|
||||
/*
|
||||
* 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";
|
||||
151
server/hybridServer.ts
Normal file
151
server/hybridServer.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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,30 +5,18 @@ 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();
|
||||
@@ -37,11 +25,16 @@ async function startServers() {
|
||||
const apiServer = createApiServer();
|
||||
const internalServer = createInternalServer();
|
||||
|
||||
let hybridClientServer;
|
||||
let nextServer;
|
||||
nextServer = await createNextServer();
|
||||
if (config.getRawConfig().traefik.file_mode) {
|
||||
const monitor = new TraefikConfigManager();
|
||||
await monitor.start();
|
||||
if (config.isManagedMode()) {
|
||||
hybridClientServer = await createHybridClientServer();
|
||||
} else {
|
||||
nextServer = await createNextServer();
|
||||
if (config.getRawConfig().traefik.file_mode) {
|
||||
const monitor = new TraefikConfigManager();
|
||||
await monitor.start();
|
||||
}
|
||||
}
|
||||
|
||||
let integrationServer;
|
||||
@@ -49,13 +42,12 @@ async function startServers() {
|
||||
integrationServer = createIntegrationApiServer();
|
||||
}
|
||||
|
||||
await initCleanup();
|
||||
|
||||
return {
|
||||
apiServer,
|
||||
nextServer,
|
||||
internalServer,
|
||||
integrationServer
|
||||
integrationServer,
|
||||
hybridClientServer
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware,
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
|
||||
import { authenticated, unauthenticated } from "@server/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 { internalRouter } from "#dynamic/routers/internal";
|
||||
import internal from "@server/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, internalRouter);
|
||||
internalServer.use(prefix, internal);
|
||||
|
||||
internalServer.use(notFoundMiddleware);
|
||||
internalServer.use(errorHandlerMiddleware);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export async function createCustomer(
|
||||
orgId: string,
|
||||
email: string | null | undefined
|
||||
): Promise<string | undefined> {
|
||||
return;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export async function getOrgTierData(
|
||||
orgId: string
|
||||
): Promise<{ tier: string | null; active: boolean }> {
|
||||
let tier = null;
|
||||
let active = false;
|
||||
|
||||
return { tier, active };
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from "./limitSet";
|
||||
export * from "./features";
|
||||
export * from "./limitsService";
|
||||
export * from "./getOrgTierData";
|
||||
export * from "./createCustomer";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { sendToClient } from "@server/routers/ws";
|
||||
import { processContainerLabels } from "./parseDockerContainers";
|
||||
import { applyBlueprint } from "./applyBlueprint";
|
||||
import { db, sites } from "@server/db";
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
domains,
|
||||
orgDomains,
|
||||
Resource,
|
||||
resourceHeaderAuth,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resourceWhitelist,
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
TargetData
|
||||
} from "./types";
|
||||
import logger from "@server/logger";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/private/certificates/createCertificate";
|
||||
import { pickPort } from "@server/routers/target/helpers";
|
||||
import { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
@@ -124,9 +123,7 @@ 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)
|
||||
@@ -267,32 +264,6 @@ 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(
|
||||
@@ -437,9 +408,7 @@ 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)
|
||||
@@ -624,25 +593,6 @@ 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,10 +42,6 @@ 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())
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,12 +3,19 @@ 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;
|
||||
|
||||
@@ -30,6 +37,19 @@ 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 ||
|
||||
@@ -89,23 +109,132 @@ 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() {
|
||||
if (build == "oss") {
|
||||
const licenseStatus = await license.check();
|
||||
if (
|
||||
!this.rawPrivateConfig.flags?.hide_supporter_key &&
|
||||
build != "oss" &&
|
||||
!licenseStatus.isHostLicensed
|
||||
) {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
}
|
||||
@@ -114,6 +243,10 @@ 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
|
||||
@@ -147,6 +280,10 @@ 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.11.0";
|
||||
export const APP_VERSION = "1.10.4";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, exitNodes, Transaction } from "@server/db";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -16,11 +16,7 @@ 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({
|
||||
@@ -59,24 +55,11 @@ export function selectBestExitNode(
|
||||
return pingResults[0];
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(
|
||||
exitNodeId: number,
|
||||
orgId: string,
|
||||
trx?: Transaction | typeof db
|
||||
): Promise<boolean> {
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveExitNodes(
|
||||
hostname: string,
|
||||
publicKey: string
|
||||
): Promise<
|
||||
{
|
||||
endpoint: string;
|
||||
publicKey: string;
|
||||
orgId: string;
|
||||
}[]
|
||||
> {
|
||||
export async function resolveExitNodes(hostname: string, publicKey: string) {
|
||||
// OSS version: simple implementation that returns empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,33 @@
|
||||
export * from "./exitNodes";
|
||||
export * from "./exitNodeComms";
|
||||
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 "./subnet";
|
||||
export * from "./getCurrentExitNodeId";
|
||||
@@ -15,9 +15,8 @@ import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { db, ExitNode, remoteExitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendToClient } from "#private/routers/ws";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "@server/lib/config";
|
||||
import { sendToClient } from "../../routers/ws";
|
||||
import { config } from "../config";
|
||||
|
||||
interface ExitNodeRequest {
|
||||
remoteType?: string;
|
||||
@@ -57,16 +56,16 @@ export async function sendToExitNode(
|
||||
} else {
|
||||
let hostname = exitNode.reachableAt;
|
||||
|
||||
// logger.debug(`Exit node details:`, {
|
||||
// type: exitNode.type,
|
||||
// name: exitNode.name,
|
||||
// reachableAt: exitNode.reachableAt,
|
||||
// });
|
||||
logger.debug(`Exit node details:`, {
|
||||
type: exitNode.type,
|
||||
name: exitNode.name,
|
||||
reachableAt: exitNode.reachableAt,
|
||||
});
|
||||
|
||||
// logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`);
|
||||
logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`);
|
||||
|
||||
if (exitNode.name == config.getRawConfig().gerbil.exit_node_name) {
|
||||
hostname = privateConfig.getRawPrivateConfig().gerbil.local_exit_node_reachable_at;
|
||||
hostname = config.getRawPrivateConfig().gerbil.local_exit_node_reachable_at;
|
||||
}
|
||||
|
||||
if (!hostname) {
|
||||
@@ -75,10 +74,10 @@ export async function sendToExitNode(
|
||||
);
|
||||
}
|
||||
|
||||
// logger.debug(`Sending request to exit node at ${hostname}`, {
|
||||
// type: request.remoteType,
|
||||
// data: request.data
|
||||
// });
|
||||
logger.debug(`Sending request to exit node at ${hostname}`, {
|
||||
type: request.remoteType,
|
||||
data: request.data
|
||||
});
|
||||
|
||||
// Handle local exit node with HTTP API
|
||||
const method = request.method || "POST";
|
||||
@@ -18,8 +18,7 @@ import {
|
||||
resources,
|
||||
targets,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
Transaction
|
||||
targetHealthCheck
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
@@ -184,47 +183,47 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud
|
||||
return [];
|
||||
}
|
||||
|
||||
// // Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails
|
||||
// const nodesWithRealOnlineStatus = await Promise.all(
|
||||
// allExitNodes.map(async (node) => {
|
||||
// // If database says it's online, verify with HTTP ping
|
||||
// let online: boolean;
|
||||
// if (filterOnline && node.type == "remoteExitNode") {
|
||||
// try {
|
||||
// const isActuallyOnline = await checkExitNodeOnlineStatus(
|
||||
// node.endpoint
|
||||
// );
|
||||
// Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails
|
||||
const nodesWithRealOnlineStatus = await Promise.all(
|
||||
allExitNodes.map(async (node) => {
|
||||
// If database says it's online, verify with HTTP ping
|
||||
let online: boolean;
|
||||
if (filterOnline && node.type == "remoteExitNode") {
|
||||
try {
|
||||
const isActuallyOnline = await checkExitNodeOnlineStatus(
|
||||
node.endpoint
|
||||
);
|
||||
|
||||
// // set the item in the database if it is offline
|
||||
// if (isActuallyOnline != node.online) {
|
||||
// await db
|
||||
// .update(exitNodes)
|
||||
// .set({ online: isActuallyOnline })
|
||||
// .where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
||||
// }
|
||||
// online = isActuallyOnline;
|
||||
// } catch (error) {
|
||||
// logger.warn(
|
||||
// `Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
// );
|
||||
// online = false;
|
||||
// }
|
||||
// } else {
|
||||
// online = node.online;
|
||||
// }
|
||||
// set the item in the database if it is offline
|
||||
if (isActuallyOnline != node.online) {
|
||||
await db
|
||||
.update(exitNodes)
|
||||
.set({ online: isActuallyOnline })
|
||||
.where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
||||
}
|
||||
online = isActuallyOnline;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
online = false;
|
||||
}
|
||||
} else {
|
||||
online = node.online;
|
||||
}
|
||||
|
||||
// return {
|
||||
// ...node,
|
||||
// online
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
return {
|
||||
...node,
|
||||
online
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const remoteExitNodes = allExitNodes.filter(
|
||||
const remoteExitNodes = nodesWithRealOnlineStatus.filter(
|
||||
(node) =>
|
||||
node.type === "remoteExitNode" && (!filterOnline || node.online)
|
||||
);
|
||||
const gerbilExitNodes = allExitNodes.filter(
|
||||
const gerbilExitNodes = nodesWithRealOnlineStatus.filter(
|
||||
(node) => node.type === "gerbil" && (!filterOnline || node.online) && !noCloud
|
||||
);
|
||||
|
||||
@@ -334,8 +333,8 @@ export function selectBestExitNode(
|
||||
return fallbackNode;
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string, trx: Transaction | typeof db = db) {
|
||||
const [exitNodeOrg] = await trx
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
const [exitNodeOrg] = await db
|
||||
.select()
|
||||
.from(exitNodeOrgs)
|
||||
.where(
|
||||
@@ -1,5 +1,8 @@
|
||||
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
|
||||
@@ -30,4 +33,32 @@ 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,3 +1,16 @@
|
||||
/*
|
||||
* 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 {
|
||||
@@ -11,5 +11,6 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./getOrgTierData";
|
||||
export * from "./createCustomer";
|
||||
export * from "./limitSet";
|
||||
export * from "./features";
|
||||
export * from "./limitsService";
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* 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,3 +1,16 @@
|
||||
/*
|
||||
* 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,3 +1,16 @@
|
||||
/*
|
||||
* 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,7 +1,21 @@
|
||||
/*
|
||||
* 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 {
|
||||
@@ -16,10 +30,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 "#dynamic/routers/ws";
|
||||
import { sendToClient } from "@server/routers/ws";
|
||||
import { build } from "@server/build";
|
||||
import { s3Client } from "@server/lib/s3";
|
||||
|
||||
interface StripeEvent {
|
||||
identifier?: string;
|
||||
@@ -31,17 +45,6 @@ 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;
|
||||
@@ -52,13 +55,11 @@ export class UsageService {
|
||||
|
||||
constructor() {
|
||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
||||
if (noop()) {
|
||||
if (build !== "saas") {
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket;
|
||||
this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath;
|
||||
|
||||
// Ensure events directory exists
|
||||
this.initializeEventsDirectory().then(() => {
|
||||
@@ -82,9 +83,7 @@ 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 {
|
||||
@@ -96,9 +95,7 @@ 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 {
|
||||
@@ -121,17 +118,15 @@ 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`
|
||||
);
|
||||
@@ -141,9 +136,7 @@ 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) {
|
||||
@@ -154,8 +147,8 @@ export class UsageService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to scan for leftover event files");
|
||||
} catch (err) {
|
||||
logger.error("Failed to scan for leftover event files:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,17 +158,17 @@ export class UsageService {
|
||||
value: number,
|
||||
transaction: any = null
|
||||
): Promise<Usage | null> {
|
||||
if (noop()) {
|
||||
if (build !== "saas") {
|
||||
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)
|
||||
@@ -198,12 +191,7 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,26 +201,25 @@ 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
|
||||
@@ -252,10 +239,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)
|
||||
@@ -272,8 +259,7 @@ export class UsageService {
|
||||
set: {
|
||||
latestValue: sql`${usage.latestValue} + ${value}`
|
||||
}
|
||||
})
|
||||
.returning();
|
||||
}).returning();
|
||||
|
||||
return returnUsage;
|
||||
}
|
||||
@@ -294,7 +280,7 @@ export class UsageService {
|
||||
value?: number,
|
||||
customerId?: string
|
||||
): Promise<void> {
|
||||
if (noop()) {
|
||||
if (build !== "saas") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -365,7 +351,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));
|
||||
}
|
||||
@@ -380,7 +366,7 @@ export class UsageService {
|
||||
meterId,
|
||||
instantaneousValue: truncatedValue,
|
||||
latestValue: truncatedValue,
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -441,7 +427,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),
|
||||
@@ -458,9 +444,7 @@ 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) {
|
||||
@@ -509,9 +493,7 @@ 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) {
|
||||
@@ -523,9 +505,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -537,9 +517,7 @@ 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) {
|
||||
@@ -559,9 +537,7 @@ 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);
|
||||
@@ -584,9 +560,7 @@ 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(
|
||||
@@ -597,7 +571,10 @@ 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);
|
||||
@@ -612,17 +589,16 @@ export class UsageService {
|
||||
|
||||
public async getUsage(
|
||||
orgId: string,
|
||||
featureId: FeatureId,
|
||||
trx: Transaction | typeof db = db
|
||||
featureId: FeatureId
|
||||
): Promise<Usage | null> {
|
||||
if (noop()) {
|
||||
if (build !== "saas") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usageId = `${orgId}-${featureId}`;
|
||||
|
||||
try {
|
||||
const [result] = await trx
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -634,9 +610,9 @@ export class UsageService {
|
||||
`Creating new usage record for ${orgId}/${featureId}`
|
||||
);
|
||||
const meterId = getFeatureMeterId(featureId);
|
||||
|
||||
|
||||
try {
|
||||
const [newUsage] = await trx
|
||||
const [newUsage] = await db
|
||||
.insert(usage)
|
||||
.values({
|
||||
usageId,
|
||||
@@ -653,7 +629,7 @@ export class UsageService {
|
||||
return newUsage;
|
||||
} else {
|
||||
// Record was created by another process, fetch it
|
||||
const [existingUsage] = await trx
|
||||
const [existingUsage] = await db
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -666,7 +642,7 @@ export class UsageService {
|
||||
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
|
||||
insertError
|
||||
);
|
||||
const [existingUsage] = await trx
|
||||
const [existingUsage] = await db
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -689,7 +665,7 @@ export class UsageService {
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
): Promise<Usage | null> {
|
||||
if (noop()) {
|
||||
if (build !== "saas") {
|
||||
return null;
|
||||
}
|
||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||
@@ -709,9 +685,7 @@ 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 {
|
||||
@@ -719,17 +693,15 @@ 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 {
|
||||
@@ -744,7 +716,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,
|
||||
@@ -760,17 +732,15 @@ 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`
|
||||
);
|
||||
@@ -785,9 +755,7 @@ 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 {
|
||||
@@ -809,25 +777,19 @@ export class UsageService {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkLimitSet(
|
||||
orgId: string,
|
||||
kickSites = false,
|
||||
featureId?: FeatureId,
|
||||
usage?: Usage,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<boolean> {
|
||||
if (noop()) {
|
||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
||||
if (build !== "saas") {
|
||||
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 trx
|
||||
orgLimits = await db
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(
|
||||
@@ -838,7 +800,7 @@ export class UsageService {
|
||||
);
|
||||
} else {
|
||||
// Get all limits set for this organization
|
||||
orgLimits = await trx
|
||||
orgLimits = await db
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(eq(limits.orgId, orgId));
|
||||
@@ -855,31 +817,16 @@ export class UsageService {
|
||||
if (usage) {
|
||||
currentUsage = usage;
|
||||
} else {
|
||||
currentUsage = await this.getUsage(
|
||||
orgId,
|
||||
limit.featureId as FeatureId,
|
||||
trx
|
||||
);
|
||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -888,24 +835,22 @@ 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 trx
|
||||
const orgSites = await db
|
||||
.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 trx
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
@@ -920,21 +865,17 @@ export class UsageService {
|
||||
};
|
||||
|
||||
// Don't await to prevent blocking
|
||||
await sendToClient(newt.newtId, payload).catch(
|
||||
(error: any) => {
|
||||
logger.error(
|
||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
);
|
||||
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) {
|
||||
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* 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 {
|
||||
@@ -15,9 +28,9 @@ import {
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { defaultRoleAllowedActions } from "@server/routers/role";
|
||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
||||
import { createCustomer } from "#dynamic/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
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";
|
||||
|
||||
export async function createUserAccountOrg(
|
||||
userId: string,
|
||||
25
server/lib/private/rateLimitStore.ts
Normal file
25
server/lib/private/rateLimitStore.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
192
server/lib/private/readConfigFile.ts
Normal file
192
server/lib/private/readConfigFile.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { Resend } from "resend";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "../config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export enum AudienceIds {
|
||||
@@ -22,7 +22,7 @@ export enum AudienceIds {
|
||||
}
|
||||
|
||||
const resend = new Resend(
|
||||
privateConfig.getRawPrivateConfig().server.resend_api_key || "missing"
|
||||
config.getRawPrivateConfig().server.resend_api_key || "missing"
|
||||
);
|
||||
|
||||
export default resend;
|
||||
@@ -11,4 +11,9 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./getTraefikConfig";
|
||||
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,13 +12,13 @@
|
||||
*/
|
||||
|
||||
import Stripe from "stripe";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { noop } from "@server/lib/billing/usageService";
|
||||
import { build } from "@server/build";
|
||||
|
||||
let stripe: Stripe | undefined = undefined;
|
||||
if (!noop()) {
|
||||
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
|
||||
if (build == "saas") {
|
||||
const stripeApiKey = config.getRawPrivateConfig().stripe?.secret_key;
|
||||
if (!stripeApiKey) {
|
||||
logger.error("Stripe secret key is not configured");
|
||||
}
|
||||
@@ -12,36 +12,42 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
||||
|
||||
export const configSchema = z
|
||||
.object({
|
||||
app: z
|
||||
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
|
||||
.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({})
|
||||
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()
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
log_level: "info",
|
||||
save_logs: false,
|
||||
log_failed_attempts: false,
|
||||
telemetry: {
|
||||
anonymous_usage: true
|
||||
}
|
||||
}),
|
||||
.optional(),
|
||||
domains: z
|
||||
.record(
|
||||
z.string(),
|
||||
@@ -55,95 +61,94 @@ 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(),
|
||||
@@ -153,32 +158,7 @@ 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
|
||||
@@ -199,10 +179,7 @@ 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)
|
||||
})
|
||||
@@ -329,7 +306,10 @@ 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;
|
||||
}
|
||||
@@ -341,14 +321,15 @@ 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"
|
||||
@@ -356,11 +337,12 @@ 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"
|
||||
|
||||
80
server/lib/remoteCertificates/certificates.ts
Normal file
80
server/lib/remoteCertificates/certificates.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
14
server/lib/remoteCertificates/index.ts
Normal file
14
server/lib/remoteCertificates/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
116
server/lib/remoteCertificates/privateCertificates.ts
Normal file
116
server/lib/remoteCertificates/privateCertificates.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
73
server/lib/remoteProxy.ts
Normal file
73
server/lib/remoteProxy.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
export enum AudienceIds {
|
||||
General = "",
|
||||
Subscribed = "",
|
||||
Churned = ""
|
||||
}
|
||||
|
||||
let resend;
|
||||
export default resend;
|
||||
|
||||
export async function moveEmailToAudience(
|
||||
email: string,
|
||||
audienceId: AudienceIds
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
export const s3Client = new S3Client({
|
||||
region: process.env.S3_REGION || "us-east-1",
|
||||
});
|
||||
@@ -9,7 +9,6 @@ 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;
|
||||
@@ -177,36 +176,17 @@ class TelemetryClient {
|
||||
|
||||
const stats = await this.getSystemStats();
|
||||
|
||||
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,
|
||||
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,
|
||||
|
||||
274
server/lib/tokenManager.ts
Normal file
274
server/lib/tokenManager.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import axios from "axios";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export interface TokenResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: {
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Manager - Handles automatic token refresh for hybrid server authentication
|
||||
*
|
||||
* Usage throughout the application:
|
||||
* ```typescript
|
||||
* import { tokenManager } from "@server/lib/tokenManager";
|
||||
*
|
||||
* // Get the current valid token
|
||||
* const token = await tokenManager.getToken();
|
||||
*
|
||||
* // Force refresh if needed
|
||||
* await tokenManager.refreshToken();
|
||||
* ```
|
||||
*
|
||||
* The token manager automatically refreshes tokens every 24 hours by default
|
||||
* and is started once in the privateHybridServer.ts file.
|
||||
*/
|
||||
|
||||
export class TokenManager {
|
||||
private token: string | null = null;
|
||||
private refreshInterval: NodeJS.Timeout | null = null;
|
||||
private isRefreshing: boolean = false;
|
||||
private refreshIntervalMs: number;
|
||||
private retryInterval: NodeJS.Timeout | null = null;
|
||||
private retryIntervalMs: number;
|
||||
private tokenAvailablePromise: Promise<void> | null = null;
|
||||
private tokenAvailableResolve: (() => void) | null = null;
|
||||
|
||||
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
|
||||
// Default to 24 hours for refresh, 5 seconds for retry
|
||||
this.refreshIntervalMs = refreshIntervalMs;
|
||||
this.retryIntervalMs = retryIntervalMs;
|
||||
this.setupTokenAvailablePromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up promise that resolves when token becomes available
|
||||
*/
|
||||
private setupTokenAvailablePromise(): void {
|
||||
this.tokenAvailablePromise = new Promise((resolve) => {
|
||||
this.tokenAvailableResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the token available promise
|
||||
*/
|
||||
private resolveTokenAvailable(): void {
|
||||
if (this.tokenAvailableResolve) {
|
||||
this.tokenAvailableResolve();
|
||||
this.tokenAvailableResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the token manager - gets initial token and sets up refresh interval
|
||||
* If initial token fetch fails, keeps retrying every few seconds until successful
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
logger.info("Starting token manager...");
|
||||
|
||||
try {
|
||||
await this.refreshToken();
|
||||
this.setupRefreshInterval();
|
||||
this.resolveTokenAvailable();
|
||||
logger.info("Token manager started successfully");
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
|
||||
this.setupRetryInterval();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up retry interval for initial token acquisition
|
||||
*/
|
||||
private setupRetryInterval(): void {
|
||||
if (this.retryInterval) {
|
||||
clearInterval(this.retryInterval);
|
||||
}
|
||||
|
||||
this.retryInterval = setInterval(async () => {
|
||||
try {
|
||||
logger.debug("Retrying initial token acquisition");
|
||||
await this.refreshToken();
|
||||
this.setupRefreshInterval();
|
||||
this.clearRetryInterval();
|
||||
this.resolveTokenAvailable();
|
||||
logger.info("Token manager started successfully after retry");
|
||||
} catch (error) {
|
||||
logger.debug("Token acquisition retry failed, will try again");
|
||||
}
|
||||
}, this.retryIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear retry interval
|
||||
*/
|
||||
private clearRetryInterval(): void {
|
||||
if (this.retryInterval) {
|
||||
clearInterval(this.retryInterval);
|
||||
this.retryInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the token manager and clear all intervals
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
this.clearRetryInterval();
|
||||
logger.info("Token manager stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current valid token
|
||||
*/
|
||||
|
||||
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
|
||||
async getToken(): Promise<string> {
|
||||
// If we don't have a token yet, wait for it to become available
|
||||
if (!this.token && this.tokenAvailablePromise) {
|
||||
await this.tokenAvailablePromise;
|
||||
}
|
||||
|
||||
if (!this.token) {
|
||||
if (this.isRefreshing) {
|
||||
// Wait for current refresh to complete
|
||||
await this.waitForRefresh();
|
||||
} else {
|
||||
throw new Error("No valid token available");
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.token) {
|
||||
throw new Error("No valid token available");
|
||||
}
|
||||
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async getAuthHeader() {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
"X-CSRF-Token": "x-csrf-protection",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh the token
|
||||
*/
|
||||
async refreshToken(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
await this.waitForRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const hybridConfig = config.getRawConfig().managed;
|
||||
|
||||
if (
|
||||
!hybridConfig?.id ||
|
||||
!hybridConfig?.secret ||
|
||||
!hybridConfig?.endpoint
|
||||
) {
|
||||
throw new Error("Hybrid configuration is not defined");
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
|
||||
|
||||
const tokenData = {
|
||||
remoteExitNodeId: hybridConfig.id,
|
||||
secret: hybridConfig.secret
|
||||
};
|
||||
|
||||
logger.debug("Requesting new token from server");
|
||||
|
||||
const response = await axios.post<TokenResponse>(
|
||||
tokenEndpoint,
|
||||
tokenData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": "x-csrf-protection"
|
||||
},
|
||||
timeout: 10000 // 10 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(
|
||||
`Failed to get token: ${response.data.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.data.data.token) {
|
||||
throw new Error("Received empty token from server");
|
||||
}
|
||||
|
||||
this.token = response.data.data.token;
|
||||
logger.debug("Token refreshed successfully");
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error updating proxy mapping:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error updating proxy mapping:", error);
|
||||
}
|
||||
|
||||
throw new Error("Failed to refresh token");
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up automatic token refresh interval
|
||||
*/
|
||||
private setupRefreshInterval(): void {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
|
||||
this.refreshInterval = setInterval(async () => {
|
||||
try {
|
||||
logger.debug("Auto-refreshing token");
|
||||
await this.refreshToken();
|
||||
} catch (error) {
|
||||
logger.error("Failed to auto-refresh token:", error);
|
||||
}
|
||||
}, this.refreshIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for current refresh operation to complete
|
||||
*/
|
||||
private async waitForRefresh(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!this.isRefreshing) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance for use throughout the application
|
||||
export const tokenManager = new TokenManager();
|
||||
@@ -6,10 +6,14 @@ import * as yaml from "js-yaml";
|
||||
import axios from "axios";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { tokenManager } from "../tokenManager";
|
||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
import { getTraefikConfig } from "./";
|
||||
import {
|
||||
getValidCertificatesForDomains,
|
||||
getValidCertificatesForDomainsHybrid
|
||||
} from "../remoteCertificates";
|
||||
import { sendToExitNode } from "../exitNodes";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export class TraefikConfigManager {
|
||||
@@ -309,92 +313,93 @@ export class TraefikConfigManager {
|
||||
this.lastActiveDomains = new Set(domains);
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.USE_PANGOLIN_DNS === "true" &&
|
||||
build != "oss"
|
||||
) {
|
||||
// Scan current local certificate state
|
||||
this.lastLocalCertificateState =
|
||||
await this.scanLocalCertificateState();
|
||||
// Scan current local certificate state
|
||||
this.lastLocalCertificateState =
|
||||
await this.scanLocalCertificateState();
|
||||
|
||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||
let validCertificates: Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
}> = [];
|
||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||
let validCertificates: Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
}> = [];
|
||||
|
||||
if (this.shouldFetchCertificates(domains)) {
|
||||
// Filter out domains that are already covered by wildcard certificates
|
||||
const domainsToFetch = new Set<string>();
|
||||
for (const domain of domains) {
|
||||
if (
|
||||
!isDomainCoveredByWildcard(
|
||||
domain,
|
||||
this.lastLocalCertificateState
|
||||
)
|
||||
) {
|
||||
domainsToFetch.add(domain);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||
);
|
||||
}
|
||||
if (this.shouldFetchCertificates(domains)) {
|
||||
// Filter out domains that are already covered by wildcard certificates
|
||||
const domainsToFetch = new Set<string>();
|
||||
for (const domain of domains) {
|
||||
if (
|
||||
!isDomainCoveredByWildcard(
|
||||
domain,
|
||||
this.lastLocalCertificateState
|
||||
)
|
||||
) {
|
||||
domainsToFetch.add(domain);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainsToFetch.size > 0) {
|
||||
// Get valid certificates for domains not covered by wildcards
|
||||
if (domainsToFetch.size > 0) {
|
||||
// Get valid certificates for domains not covered by wildcards
|
||||
if (config.isManagedMode()) {
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomainsHybrid(
|
||||
domainsToFetch
|
||||
);
|
||||
} else {
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomains(
|
||||
domainsToFetch
|
||||
);
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
|
||||
logger.info(
|
||||
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||
);
|
||||
|
||||
// Download and decrypt new certificates
|
||||
await this.processValidCertificates(validCertificates);
|
||||
} else {
|
||||
logger.info(
|
||||
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||
);
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
}
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
|
||||
// Always ensure all existing certificates (including wildcards) are in the config
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
logger.info(
|
||||
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||
);
|
||||
|
||||
// Download and decrypt new certificates
|
||||
await this.processValidCertificates(validCertificates);
|
||||
} else {
|
||||
const timeSinceLastFetch = this.lastCertificateFetch
|
||||
? Math.round(
|
||||
(Date.now() -
|
||||
this.lastCertificateFetch.getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: 0;
|
||||
|
||||
// logger.debug(
|
||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||
// );
|
||||
|
||||
// Still need to ensure config is up to date with existing certificates
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
logger.info(
|
||||
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||
);
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
}
|
||||
|
||||
// Clean up certificates for domains no longer in use
|
||||
await this.cleanupUnusedCertificates(domains);
|
||||
// Always ensure all existing certificates (including wildcards) are in the config
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
} else {
|
||||
const timeSinceLastFetch = this.lastCertificateFetch
|
||||
? Math.round(
|
||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: 0;
|
||||
|
||||
// wait 1 second for traefik to pick up the new certificates
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// logger.debug(
|
||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||
// );
|
||||
|
||||
// Still need to ensure config is up to date with existing certificates
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
}
|
||||
|
||||
// Clean up certificates for domains no longer in use
|
||||
await this.cleanupUnusedCertificates(domains);
|
||||
|
||||
// wait 1 second for traefik to pick up the new certificates
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Write traefik config as YAML to a second dynamic config file if changed
|
||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||
|
||||
@@ -443,15 +448,32 @@ export class TraefikConfigManager {
|
||||
} | null> {
|
||||
let traefikConfig;
|
||||
try {
|
||||
const currentExitNode = await getCurrentExitNodeId();
|
||||
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||
traefikConfig = await getTraefikConfig(
|
||||
// this is called by the local exit node to get its own config
|
||||
currentExitNode,
|
||||
config.getRawConfig().traefik.site_types,
|
||||
build == "oss", // filter out the namespace domains in open source
|
||||
build != "oss" // generate the login pages on the cloud and hybrid
|
||||
);
|
||||
if (config.isManagedMode()) {
|
||||
const resp = await axios.get(
|
||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`,
|
||||
await tokenManager.getAuthHeader()
|
||||
);
|
||||
|
||||
if (resp.status !== 200) {
|
||||
logger.error(
|
||||
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`,
|
||||
{ responseData: resp.data }
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
traefikConfig = resp.data.data;
|
||||
} else {
|
||||
const currentExitNode = await getCurrentExitNodeId();
|
||||
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||
traefikConfig = await getTraefikConfig(
|
||||
// this is called by the local exit node to get its own config
|
||||
currentExitNode,
|
||||
config.getRawConfig().traefik.site_types,
|
||||
build == "oss", // filter out the namespace domains in open source
|
||||
build != "oss" // generate the login pages on the cloud and hybrid
|
||||
);
|
||||
}
|
||||
|
||||
const domains = new Set<string>();
|
||||
|
||||
@@ -696,12 +718,7 @@ export class TraefikConfigManager {
|
||||
|
||||
for (const cert of validCertificates) {
|
||||
try {
|
||||
if (
|
||||
!cert.certFile ||
|
||||
!cert.keyFile ||
|
||||
cert.certFile.length === 0 ||
|
||||
cert.keyFile.length === 0
|
||||
) {
|
||||
if (!cert.certFile || !cert.keyFile) {
|
||||
logger.warn(
|
||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||
);
|
||||
@@ -825,9 +842,7 @@ export class TraefikConfigManager {
|
||||
const lastUpdateStr = fs
|
||||
.readFileSync(lastUpdatePath, "utf8")
|
||||
.trim();
|
||||
lastUpdateTime = Math.floor(
|
||||
new Date(lastUpdateStr).getTime() / 1000
|
||||
);
|
||||
lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000);
|
||||
} catch {
|
||||
lastUpdateTime = null;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { db, targetHealthCheck } from "@server/db";
|
||||
import {
|
||||
and,
|
||||
eq,
|
||||
inArray,
|
||||
or,
|
||||
isNull,
|
||||
ne,
|
||||
isNotNull,
|
||||
desc,
|
||||
sql
|
||||
} from "drizzle-orm";
|
||||
import { db, exitNodes, targetHealthCheck } from "@server/db";
|
||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { resources, sites, Target, targets } from "@server/db";
|
||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||
import { build } from "@server/build";
|
||||
import createPathRewriteMiddleware from "./middleware";
|
||||
import { sanitize, validatePathRewriteConfig } from "./utils";
|
||||
|
||||
@@ -88,13 +79,7 @@ export async function getTraefikConfig(
|
||||
and(
|
||||
eq(targets.enabled, true),
|
||||
eq(resources.enabled, true),
|
||||
or(
|
||||
eq(sites.exitNodeId, exitNodeId),
|
||||
and(
|
||||
isNull(sites.exitNodeId),
|
||||
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes
|
||||
)
|
||||
),
|
||||
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)),
|
||||
or(
|
||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||
@@ -120,12 +105,7 @@ export async function getTraefikConfig(
|
||||
const priority = row.priority ?? 100;
|
||||
|
||||
// Create a unique key combining resourceId, path config, and rewrite config
|
||||
const pathKey = [
|
||||
targetPath,
|
||||
pathMatchType,
|
||||
rewritePath,
|
||||
rewritePathType
|
||||
]
|
||||
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
|
||||
.filter(Boolean)
|
||||
.join("-");
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
@@ -140,15 +120,13 @@ export async function getTraefikConfig(
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
logger.error(
|
||||
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
|
||||
);
|
||||
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
resourcesMap.set(key, {
|
||||
resourceId: row.resourceId,
|
||||
name: resourceName,
|
||||
name: resourceName,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
http: row.http,
|
||||
@@ -180,6 +158,9 @@ export async function getTraefikConfig(
|
||||
port: row.port,
|
||||
internalPort: row.internalPort,
|
||||
enabled: row.targetEnabled,
|
||||
rewritePath: row.rewritePath,
|
||||
rewritePathType: row.rewritePathType,
|
||||
priority: row.priority,
|
||||
site: {
|
||||
siteId: row.siteId,
|
||||
type: row.siteType,
|
||||
@@ -258,18 +239,21 @@ export async function getTraefikConfig(
|
||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||
}
|
||||
|
||||
const tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
let tls = {};
|
||||
if (build == "oss") {
|
||||
tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
@@ -280,12 +264,11 @@ export async function getTraefikConfig(
|
||||
];
|
||||
|
||||
// Handle path rewriting middleware
|
||||
if (
|
||||
resource.rewritePath !== null &&
|
||||
resource.path !== null &&
|
||||
if (resource.rewritePath &&
|
||||
resource.path &&
|
||||
resource.pathMatchType &&
|
||||
resource.rewritePathType
|
||||
) {
|
||||
resource.rewritePathType) {
|
||||
|
||||
// Create a unique middleware name
|
||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
||||
|
||||
@@ -304,10 +287,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
|
||||
// the middleware to the config
|
||||
Object.assign(
|
||||
config_output.http.middlewares,
|
||||
rewriteResult.middlewares
|
||||
);
|
||||
Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
|
||||
|
||||
// middlewares to the router middleware chain
|
||||
if (rewriteResult.chain) {
|
||||
@@ -318,13 +298,9 @@ export async function getTraefikConfig(
|
||||
routerMiddlewares.push(rewriteMiddlewareName);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||
);
|
||||
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||
);
|
||||
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +316,7 @@ export async function getTraefikConfig(
|
||||
value: string;
|
||||
}[];
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||
);
|
||||
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
|
||||
}
|
||||
|
||||
headersArr.forEach((header) => {
|
||||
@@ -508,14 +482,14 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.stickySession
|
||||
? {
|
||||
sticky: {
|
||||
cookie: {
|
||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||
secure: resource.ssl,
|
||||
httpOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
sticky: {
|
||||
cookie: {
|
||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||
secure: resource.ssl,
|
||||
httpOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
};
|
||||
@@ -616,13 +590,13 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.stickySession
|
||||
? {
|
||||
sticky: {
|
||||
ipStrategy: {
|
||||
depth: 0,
|
||||
sourcePort: true
|
||||
}
|
||||
}
|
||||
}
|
||||
sticky: {
|
||||
ipStrategy: {
|
||||
depth: 0,
|
||||
sourcePort: true
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user